RORK LABJP
FOUNDATION — The model layer underneath every AI app builder moved again. Gemini 3.8 Flash reached general availability on September 2, aimed squarely at long-horizon coding and autonomous agentsRECORD — The quality of generated code shifts quietly alongside updates like these. Keeping your own note of when something changed makes it far easier to trace a difference in behavior laterMAX — Rork Max writes native Swift and compiles it on a cloud Mac fleet. A live simulator streams to your browser, and you can publish to the App Store without ever opening XcodeDEPTH — Its reach into native capabilities is the real draw: LiDAR, Dynamic Island, Live Activities, HealthKit, widgets, App Clips, and on-device inference through Core MLPRICING — Max starts at $200 a month and runs up to $1,800 at higher tiers, with a free tier of roughly five prompts a week. For solo developers, working out how far the free tier gets you is a sensible first stepSTACK — Standard Rork is built on React Native and Expo, aiming for a genuinely native feel rather than a web wrapper. Max is the choice when you need to reach deeper into the platformFOUNDATION — The model layer underneath every AI app builder moved again. Gemini 3.8 Flash reached general availability on September 2, aimed squarely at long-horizon coding and autonomous agentsRECORD — The quality of generated code shifts quietly alongside updates like these. Keeping your own note of when something changed makes it far easier to trace a difference in behavior laterMAX — Rork Max writes native Swift and compiles it on a cloud Mac fleet. A live simulator streams to your browser, and you can publish to the App Store without ever opening XcodeDEPTH — Its reach into native capabilities is the real draw: LiDAR, Dynamic Island, Live Activities, HealthKit, widgets, App Clips, and on-device inference through Core MLPRICING — Max starts at $200 a month and runs up to $1,800 at higher tiers, with a free tier of roughly five prompts a week. For solo developers, working out how far the free tier gets you is a sensible first stepSTACK — Standard Rork is built on React Native and Expo, aiming for a genuinely native feel rather than a web wrapper. Max is the choice when you need to reach deeper into the platform
Articles/Dev Tools
Dev Tools/2026-09-05Advanced

Your Widget Extension Can Submit a BGTaskScheduler Request. It Just Cannot Register the Handler

Calling BGTaskScheduler from a widget extension compiles, submits, and returns true — and then nothing runs. Here is why registration belongs to the host app only, and how I moved refresh ownership back where it belongs across my wallpaper apps.

Rork552WidgetKit11BGTaskScheduler4iOS112Expo200App Extension3

Premium Article

The week after I finished chasing down a widget that froze on yesterday's image every evening, a new request arrived: the wallpaper should already be the next one by morning, even on days the app is never opened.

The obvious reading is that the widget owns the refresh. The widget is what draws the image, so the widget should fetch the next one — that was my reasoning when I added a BGAppRefreshTaskRequest to the widget extension target.

It compiled. submit returned true. The next morning the screen was unchanged, and it took me half a day to find out why.

Submit is allowed. Only register is refused

Here is the short version. BGTaskScheduler lets an extension ask for work, but it does not let an extension be the one that receives it. In the SDK header, registerForTaskWithIdentifier:usingQueue:launchHandler: carries an extension-unavailable annotation with a single line of explanation: "Only the host application may register launch handlers" (BGTaskScheduler.h in BackgroundTasks.framework).

The discussion text right below it adds that while some extensions may submit task requests, only the host app is ever launched to perform the background work. So a successful submit means "request accepted" and nothing more.

CallFrom a widget extensionWhat actually happens
submitTaskRequest:error:AllowedThe request is accepted and you get true back
registerForTaskWithIdentifier:...Not allowedMarked unavailable to extensions
Target that gets launchedThe host app only; the extension is never woken

This is the code I wrote. It runs. There is simply nobody on the receiving end.

// WallpaperWidget/RefreshScheduler.swift — this design is a dead end
import BackgroundTasks
 
enum RefreshScheduler {
    static func scheduleFromWidget() {
        let request = BGAppRefreshTaskRequest(identifier: "net.dolice.wallpaper.dailyRefresh")
        request.earliestBeginDate = Calendar.current.date(byAdding: .hour, value: 6, to: Date())
        do {
            try BGTaskScheduler.shared.submit(request)
            // This line is reached. true comes back, the log says "scheduled".
            // But no launchHandler can be registered here, so nothing ever wakes up.
            NSLog("[widget] submitted dailyRefresh")
        } catch {
            NSLog("[widget] submit failed: \(error)")
        }
    }
}

Because submit never throws here, the log only ever records success. Nothing crashes, so the problem stays invisible. Silent failures like this are the ones I have learned to distrust most.

The identifier allow-list lives in the app's Info.plist

There is a second wall waiting if you try to keep refresh inside the extension, and it is the permitted-identifier list.

BGTaskScheduler returns NotPermitted (Code=3) when the right mode is missing from UIBackgroundModes, or when the identifier is absent from BGTaskSchedulerPermittedIdentifiers. The detail that matters: that array belongs in the app's Info.plist. Reading it alongside the unavailable error code reference makes the split between Code=1 and Code=3 much clearer.

Adding it to the extension's Info.plist does nothing

It is tempting to select the widget extension target in Xcode and put BGTaskSchedulerPermittedIdentifiers in its Info.plist. I did exactly that once. No error appears. Nothing happens either.

On Rork and Expo projects, put it in app.config

Rork emits a React Native and Expo project, so rather than hand-editing Info.plist I keep this in the config file.

// app.config.js — this lands in the host app's Info.plist
export default {
  expo: {
    ios: {
      bundleIdentifier: "net.dolice.wallpaper",
      infoPlist: {
        UIBackgroundModes: ["fetch", "processing"],
        BGTaskSchedulerPermittedIdentifiers: [
          "net.dolice.wallpaper.dailyRefresh"
        ]
      }
    }
  }
};

Define the identifier once and read it from both targets

If the identifier string drifts between the host app and the extension, register and submit end up pointing at different tasks, and that mismatch is silent too. I keep the constants in a shared target that both sides include.

// Shared/RefreshTaskID.swift — included in both the app and the widget extension
public enum RefreshTaskID {
    public static let daily = "net.dolice.wallpaper.dailyRefresh"
    public static let appGroup = "group.net.dolice.wallpaper"
    public static let widgetKind = "DailyWallpaperWidget"
}

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
You will be able to tell within 5 minutes whether register or submit is the call your setup is not allowed to make, instead of losing half a day to a silent no-op
You will be able to separate the two budgets at play — the chance a BGAppRefreshTask gets to run, and WidgetKit's reload allowance — and design the handoff from host app to widget deliberately
You will know whether to inspect getPendingTaskRequests or your App Group write record first when submit returned true but the widget never changed
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $15 for lifetime access
View Membership →

Related Articles

Dev Tools2026-06-15
Putting a Working Button in a Rork App's Widget — Implementing App Intents So a Tap Acts Without Opening the App
How to put a button in a Rork-generated Expo app's widget that changes state without launching the app. We wire App Intents and WidgetKit together through an App Group, all the way to reloadTimelines — including the two places I lost real time.
Dev Tools2026-06-14
Actually Delivering 'It Updates Without Opening' in Expo — A Realistic Background Task Design
Building 'content refreshes every morning' into a Rork-generated Expo app runs into iOS background execution being far less dutiful than you expect. Here is a minimal expo-background-task setup plus a design that doesn't break when the task never runs.
Dev Tools2026-06-12
Adding a Home Screen Widget to a Rork App — Making WidgetKit Work Within Expo's Constraints
Rork generates Expo apps, and home screen widgets can't be written in React Native. Here's how to wire up WidgetKit with a config plugin and App Groups — including the parts that tripped me up.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →