FI
ForgeOrchestrator

SequenceOrchestrator

Runs actions once, sequentially, in priority order. Built for app launch gates.

When to Use

Use SequenceOrchestrator when you have a set of one-shot actions that need to run in a defined order — like the gates that appear between app launch and the home screen. Each action runs to completion before the next begins.

Basic Usage

import ForgeOrchestrator

let orchestrator = SequenceOrchestrator()

orchestrator.register(ForceUpdateAction())
orchestrator.register(OnboardingAction())
orchestrator.register(WhatsNewAction())

// Runs eligible actions in priority order, one at a time
let executed: [ActionID] = await orchestrator.evaluate()

Batch registration

Register multiple actions at once with the array overload.

orchestrator.register([
    ForceUpdateAction(),
    OnboardingAction(),
    WhatsNewAction(),
])

Evaluation Lifecycle

  1. Condition evaluationshouldRun() is called on every registered action concurrently. Cheap, idempotent, and parallel.
  2. Priority sorting — eligible actions are sorted .critical → .low. Ties preserve registration order.
  3. Sequential execution — actions run one at a time, in priority order. Each execute() must return before the next begins.
  4. Completionevaluate() returns the IDs of every action that ran.

Reentrancy

Concurrent evaluate() calls are ignored. If one evaluation is already in progress and you call evaluate() again, the second call returns an empty array immediately. Check isProcessing if you want to branch.

Observable State

SequenceOrchestrator is @Observable @MainActor. Bind its published state directly to SwiftUI views for progress UI.

PropertyTypeMeaning
isProcessingBoolTrue while evaluation is running
currentActionIdActionID?The action currently executing (or nil)
eligibleCountIntActions that passed shouldRun() in the current run
completedCountIntActions that finished executing so far
struct StartupProgressView: View {
    @Bindable var orchestrator: SequenceOrchestrator

    var body: some View {
        if orchestrator.isProcessing {
            VStack {
                ProgressView(
                    value: Double(orchestrator.completedCount),
                    total: Double(orchestrator.eligibleCount)
                )
                if let current = orchestrator.currentActionId {
                    Text("Running: \(current.rawValue)")
                }
            }
        }
    }
}

Re-evaluation

A single orchestrator can run multiple evaluation cycles across the app's lifetime. For example, after the user completes onboarding, tear down the onboarding action and evaluate a different set.

// After state changes (e.g. user completed onboarding),
// tear down and re-evaluate to pick up newly eligible actions
orchestrator.removeAll()
orchestrator.register([WhatsNewAction(), ReviewPromptAction()])
await orchestrator.evaluate()

Bridging UI with CompletionSignal

When an action shows a UI flow and needs to wait for the user, use CompletionSignal to bridge async code with the completion callback.

struct OnboardingAction: SequenceAction {
    let id: ActionID = "onboarding"
    let priority: ActionPriority = .high

    func shouldRun() async -> Bool {
        !UserDefaults.standard.bool(forKey: "onboarded")
    }

    func execute() async {
        // Present UI, then wait for the user to finish
        let signal = CompletionSignal()
        OnboardingPresenter.show { signal.complete() }
        await signal.wait()
        UserDefaults.standard.set(true, forKey: "onboarded")
    }
}

Don't block forever. If execute() suspends indefinitely (e.g., a CompletionSignal that's never completed), the entire queue stalls. Always give the user a path to resolve the flow.

Best Practices

  • One evaluate per launch cycle. Don't call evaluate() in a loop — if you need continuous re-checking, use MonitorOrchestrator instead.
  • Let .critical be truly critical. Reserve it for actions that must complete before anything else runs. Overusing .critical defeats the point of the ordering.
  • Make actions independent. SequenceOrchestrator has no shared state between actions. If action B needs data from action A, use PipelineOrchestrator instead.
  • Observe from SwiftUI. Use the @Observable properties for progress UI rather than polling or relying on callbacks.