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
- Condition evaluation —
shouldRun()is called on every registered action concurrently. Cheap, idempotent, and parallel. - Priority sorting — eligible actions are sorted
.critical → .low. Ties preserve registration order. - Sequential execution — actions run one at a time, in priority order. Each
execute()must return before the next begins. - Completion —
evaluate()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.
| Property | Type | Meaning |
|---|---|---|
isProcessing | Bool | True while evaluation is running |
currentActionId | ActionID? | The action currently executing (or nil) |
eligibleCount | Int | Actions that passed shouldRun() in the current run |
completedCount | Int | Actions 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, useMonitorOrchestratorinstead. - Let
.criticalbe truly critical. Reserve it for actions that must complete before anything else runs. Overusing.criticaldefeats the point of the ordering. - Make actions independent. SequenceOrchestrator has no shared state between actions. If action B needs data from action A, use
PipelineOrchestratorinstead. - Observe from SwiftUI. Use the
@Observableproperties for progress UI rather than polling or relying on callbacks.