Skip to content

Signals, Timers & Queries

Workflow steps can suspend waiting for external events. When a step suspends, the runtime persists the suspension marker to the journal and frees the actor. On resume, the runtime restores the step’s execution state and continues. All four primitives below are durable — they survive node restarts.

perform Signal.wait(name) suspends the workflow step until the named signal arrives:

workflow Signaled {
step wait_for_go {
perform Signal.wait("go")
}
}

When the step executes Signal.wait("go"), the actor suspends and records the signal name in the journal. Delivering the signal (rt.signal_workflow(actor_id, "go", None)) resumes the step, which continues past the perform.

A step that waits on two signals in sequence suspends twice:

workflow TwoSignals {
step wait_for_both {
(perform Signal.wait("first"), perform Signal.wait("second"))
}
}

The step suspends on "first", resumes when it arrives, then suspends again on "second". Both suspension markers are journaled, so a restart between the two waits resumes correctly — the second wait is re-captured, not dropped.

After a node restart, signals that were delivered but not yet processed are replayed from the journal. The recovered workflow re-arms the wait and the step completes when the replayed signal arrives.

perform Timer.sleep(name, duration_ms) schedules a durable workflow timer:

workflow TimerWorkflow {
step wait { perform Timer.sleep("timeout1", 1) }
}

The timer is journaled with its name and duration. The actor suspends. When the timer fires, the runtime resumes the step.

On recovery, durable timers are re-armed from the journal. If the timer should have already fired (the duration elapsed during the downtime), it fires immediately on recovery. This guarantees a workflow never gets stuck waiting for a timer that already expired.

perform LLM.ask(prompt) sends the prompt to the configured LLM and suspends the step until the response arrives:

workflow LlmFlow {
step ask_step { self.answer = perform LLM.ask("hello") }
}

Unlike a blocking ask to an agent, LLM.ask inside a workflow step suspends non-blockingly. The runtime spawns a worker thread for the HTTP call, the actor suspends, and the step resumes when the LLM response arrives.

A step can perform LLM.ask and Signal.wait in sequence:

workflow SignalThenLlm {
step wait_then_ask {
(perform Signal.wait("go"), self.answer = perform LLM.ask("hello"))
}
}

The step suspends on the signal, resumes when it arrives, then suspends again on the LLM call. Both suspension markers are journaled. If the node restarts between the signal and the LLM call, the recovered step re-issues the LLM call and completes normally.

When the agent or workflow has a pricing configuration, LLM calls record token costs for usage tracking. The cost is written back to the agent’s memory subsystem after the call completes.

perform Workflow.query(self, "name") invokes a registered query handler on the workflow actor:

workflow Counter {
step bump { self.step_index = self.step_index + 1 }
step inspect { self.observed = perform Workflow.query(self, "progress") }
}
fn progress() -> Int { self.step_index }

Query handlers are plain functions that read self state. The runtime invokes the handler with the workflow actor bound as self, so it observes the actor’s current state without mutating it.

  • Read-only: queries do not append workflow events or advance step_index.
  • Unknown names return nil: querying an unregistered name returns nil, not an error.
  • Runs on a private VM: the query handler executes on a separate VM instance, so it cannot disturb the step’s own execution state.

Query handlers are registered programmatically via the runtime API:

rt.register_workflow_query(actor_id, "progress", handler);
let result = rt.query_workflow(actor_id, "progress");

The handler is a function value (a function-table index) returned from the program entry. In typical usage, the program entry returns the handler function and the host registers it before invoking queries.

Effect Operation Signature Description
Signal wait wait(name: String) -> Unit Suspend until the named signal arrives
Timer sleep sleep(name: String, duration_ms: Int) -> Unit Schedule a durable workflow timer
LLM ask ask(prompt: String) -> String Non-blocking LLM call; suspends until response
Workflow query query(self, name: String) -> Value Read-only state query via registered handler

These operations are only available inside workflow actors (runtime-host context). Outside a workflow, they are nil no-ops.