Events

Create and route custom events in Swift

Events

Events are the unit of work in a Blazen workflow. Every step consumes events of one or more declared types and emits zero, one, or many events in response.

The Event record

Event is a simple value type re-exported from the underlying UniFFI glue:

public struct Event: Codable, Sendable {
    let eventType: String   // class name such as "blazen::StartEvent" or "AnalyzeEvent"
    let dataJson: String    // JSON-encoded payload
}

The Swift wrapper makes Event Codable for you, so it round-trips cleanly through JSONEncoder / JSONDecoder if you ever need to log or persist one.

Built-in event types

Two event-type strings are special-cased by the engine:

Wire typeRole
"blazen::StartEvent"Emitted when the workflow begins. Carries the JSON payload you passed to workflow.run(_:).
"blazen::StopEvent"Returning an Event whose eventType contains StopEvent terminates the workflow. Attach your final output under the "result" key.

For your own routing events, the eventType can be any string — typically the type name of a Swift struct you’ve defined to wrap the payload:

struct AnalyzeEvent: Codable {
    let text: String
    let score: Double
}

// Emit one.
let payload = AnalyzeEvent(text: "hello", score: 0.9)
let data = try JSONEncoder().encode(payload)
let event = Event(
    eventType: "AnalyzeEvent",
    dataJson: String(data: data, encoding: .utf8) ?? "{}"
)

Implementing StepHandler

Every step has a handler conforming to StepHandler. The handler receives the incoming event, does whatever work it wants, and returns a StepOutput:

final class AnalyzeHandler: StepHandler, @unchecked Sendable {
    func invoke(event: Event) async throws -> StepOutput {
        let parsed = try JSONDecoder().decode(
            AnalyzeInput.self,
            from: Data(event.dataJson.utf8)
        )
        let scored = AnalyzeEvent(text: parsed.text, score: classify(parsed.text))
        let data = try JSONEncoder().encode(scored)
        let json = String(data: data, encoding: .utf8) ?? "{}"
        return .single(event: Event(eventType: "AnalyzeEvent", dataJson: json))
    }
}

private struct AnalyzeInput: Codable { let text: String }

The @unchecked Sendable annotation is the canonical idiom for handler classes — the handler is invoked from the Tokio runtime that backs the workflow, so the underlying type must be sendable across actor boundaries. Use it for handlers that don’t hold non-Sendable state; for handlers that do, lock the state behind an actor and have invoke(event:) await into the actor.

Returning events from a step

StepOutput is an enum with three variants:

return .single(event: nextEvent)                  // emit one event
return .multiple(events: [branchA, branchB])      // fan out to multiple steps
return .none                                       // side-effect step, no follow-up event

A handler that wants to do real work without scheduling another step (e.g. to write to a sink, fire telemetry, or update an external store) returns .none.

Event routing via accepts / emits

Routing is declared at builder time, not derived from the handler signature. Each step states the event types it accepts and the event types it might emit:

let builder = WorkflowBuilder(name: "analyzer")
_ = try builder.step(
    name: "parse",
    accepts: ["blazen::StartEvent"],
    emits: ["AnalyzeEvent"],
    handler: ParseHandler()
)
_ = try builder.step(
    name: "score",
    accepts: ["AnalyzeEvent"],
    emits: ["blazen::StopEvent"],
    handler: ScoreHandler()
)
let workflow = try builder.build()

The engine validates the graph at build() time — a step whose emits list contains an event type that no other step accepts is a build-time error (unless that event terminates the workflow, like blazen::StopEvent).

Fan-out

A step that emits multiple events at once routes each event independently. Both downstream steps run — in parallel where the runtime can, sequentially otherwise:

final class FanOutHandler: StepHandler, @unchecked Sendable {
    func invoke(event: Event) async throws -> StepOutput {
        return .multiple(events: [
            Event(eventType: "BranchA", dataJson: "{\"value\":\"a\"}"),
            Event(eventType: "BranchB", dataJson: "{\"value\":\"b\"}"),
        ])
    }
}

Declare both event types in the step’s emits list so the engine knows about them.

Inspecting registered steps

A built workflow exposes its step names as a property — useful for logging or debugging:

let workflow = try builder.build()
print(workflow.steps)   // ["parse", "score"]

This re-exposes the underlying stepNames() method as a property so call sites match Swift naming conventions.

See also

  • Context — share state between steps.
  • Streaming — stream chunks out of LLM completions.
  • Agent — drive a tool-call loop end-to-end.