Skip to main content

What is a Stream Step?

A stream step is a long-lived reactive unit that subscribes to a named stream and runs in response to signals. Unlike a regular @step — which runs once when called from a workflow — a @stream_step is materialized as a subscription row when its parent stream workflow starts, and is then re-invoked each time a matching signal arrives, until it explicitly terminates.

Key Characteristics

Signal-driven

The step body runs once on start and again on each matching signal arrival.

Stateful

Use save_checkpoint() / get_checkpoint() to carry state across resumes without rebuilding from scratch.

Suspendable

suspend() parks the step and bubbles a SuspensionSignal up to the parent workflow — perfect for HITL.

Schema-aware

Bind Pydantic models to signal types and the dispatcher validates payloads before delivery.

The Two Code Paths

A stream step has two code paths the runtime invokes at different times:
  1. on_signal callback — runs on every matching signal arrival. It receives the raw Signal and a StreamStepContext. Its job is to decide whether to resume(), cancel(), terminate(), or suspend() — typically very lightweight.
  2. The lifecycle function (the decorated function itself) — runs on first start and again on each explicit ctx.resume(). This is where the actual work happens.
If you don’t pass an on_signal, the default callback (_auto_resume_on_signal) just calls ctx.resume() for every matching signal — which is the right thing 90% of the time.

Defining Signals

The signals= argument accepts either a plain list of names or a dict mapping names to Pydantic schemas.
When a schema is bound to a signal type, payloads that fail validation are rejected by the dispatcher and the step is not invoked for that signal.

Lifecycle Primitives

These helpers, imported from pyworkflow.streams, are how the lifecycle function communicates with the dispatcher. They all set state that the dispatcher reads after the function returns — they don’t immediately mutate the subscription row.

The Signal Object

get_current_signal() returns a Signal dataclass:
The sequence field gives you a strict ordering across all signals on the stream — useful for deduping or detecting gaps. source_run_id is set automatically when emit() is called from inside a workflow context.

Emitting Signals from a Step

Stream steps can emit signals back to the same or another stream — this is how multi-actor pipelines fan out work between participants.
emit() automatically picks up the current stream_run_id from context, so signals stay scoped to the same run unless you pass an explicit override.

The StreamStepContext (for on_signal callbacks)

When you pass a custom on_signal=..., the callback receives (signal, ctx) where ctx is a StreamStepContext: ctx also exposes ctx.status, ctx.run_id, and ctx.stream_id for inspection.

Terminal States

Every stream step subscription is in one of these states. The aggregate of all step states determines whether the parent stream workflow is running, completed, or suspended — see Stream Workflows: Aggregate Lifecycle.

Best Practices

  • Keep on_signal callbacks lightweight. They run for every signal, including ones you ignore — do filtering there, do work in the lifecycle function.
  • Always handle the signal is None case. The first invocation has no triggering signal; that’s the registration phase. Use it to seed state, not to do work.
  • Use set_result() instead of save_checkpoint() for output the parent needs. save_checkpoint is for internal state across resumes; set_result is the parent-facing return value.
  • Call terminate() or suspend() when you’re done. A step that just returns without setting a terminal state stays in waiting and keeps the parent stream workflow alive.
  • Don’t rely on globals. Step lifecycles can run on different workers between resumes — persist anything you need via save_checkpoint().
  • Stream Workflows — the parent runtime that hosts and drives stream steps
  • Steps — the regular, run-once step model for sequential workflows
  • Hooks — the suspension primitive that stream workflows use under the hood