◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Expo SDK 58 is out of beta as of 10/15 (beta since 09/15). Check your dependencies before upgrading●SDK 58 ISSUE — Early reports include Android widgets not refreshing on a schedule and Stripe linking statically●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Expo SDK 58 is out of beta as of 10/15 (beta since 09/15). Check your dependencies before upgrading●SDK 58 ISSUE — Early reports include Android widgets not refreshing on a schedule and Stripe linking statically●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
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.

Rork594WidgetKit11BGTaskScheduler4iOS116Expo223App 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 launched—The 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 $10 for lifetime access
View Membership →

Related Articles

⬡ Dev Tools2026-05-28
Tracking Down BGTaskScheduler.submit Error Code=1 (Unavailable) in Rork iOS Apps
When BGTaskScheduler.submit returns Error Code=1, the cause space is finite — six of them. Covers telling Code=1 from Code=3, the identifier trap in Expo-based Rork apps, submitting from a widget extension by mistake, and a getPendingTaskRequests self-check.
⬡ Dev Tools2026-09-21
A build that stops with exit code 0 — the flag that silenced the log, and the check that let an empty file through
Two reports landed in the same week: eas build --local stopping with exit code 0 but produced no further output, and a zero-byte stub that never got embedded. Both came down to settings that reduce output. Here is how to audit your own scripts.
⬡ Dev Tools2026-07-27
When to Raise Your Minimum iOS Version — Count Leftover Branches, Not User Percentages
Judging a minimum OS bump by usage share produces the same answer every year, so the decision never happens. Here is the annotation convention, the sweep script that counts how many iOS and Android branches each candidate floor would retire, and what to watch for 30 days after.