Interrupts¶
An interrupt pauses workflow execution and waits for an external response before continuing.
Use interrupts for:
- approval gates
- human review
- external callbacks
- pause-and-resume workflow patterns
Spectra supports two interrupt styles:
- declarative — configure the pause on the node
- programmatic — trigger the pause from step code
Choose the interrupt style¶
| Style | Best for |
|---|---|
| Declarative | Simple approval points before or after a node |
| Programmatic | Steps that need to decide at runtime whether to pause |
Declarative interrupts¶
Declarative interrupts let you pause without writing step code.
You configure the node with InterruptBefore or InterruptAfter.
var workflow = WorkflowBuilder.Create("review-pipeline")
.AddNode("generate", "prompt", node => node
.WithParameter("userPrompt", "Write a report on {{inputs.topic}}"))
.AddNode("publish", "prompt", node => node
.WithInterruptBefore("Review the generated report before publishing")
.WithParameter("userPrompt", "Format for publication: {{nodes.generate.output.response}}"))
.AddEdge("generate", "publish")
.Build();
When they pause¶
| Interrupt | When it pauses | Outputs applied? |
|---|---|---|
InterruptBefore |
Before the step runs | No |
InterruptAfter |
After the step runs, before edge evaluation | Yes |
This is the simplest way to add approval gates into a workflow.
Programmatic interrupts¶
Programmatic interrupts are triggered from inside step code.
Use them when the step needs to decide at runtime whether a human or external system should review something.
public async Task<StepResult> ExecuteAsync(StepContext context)
{
var plan = GenerateDeploymentPlan(context);
var response = await context.InterruptAsync("deployment-approval", b => b
.WithTitle("Approve Deployment Plan")
.WithPayload(new { plan, estimatedCost = "$42.50" }));
if (response.Rejected)
return StepResult.Fail("Deployment rejected: " + response.Comment);
return StepResult.Success(new() { ["approved"] = true });
}
When execution resumes, the InterruptAsync(...) call returns the response and the step continues from there.
That makes programmatic interrupts feel like a normal async pause point in your code.
What happens when an interrupt is raised¶
When an interrupt occurs, the runner:
- captures the interrupt request
- checkpoints the workflow state
- marks the run as interrupted
- stops execution until a response is provided
That is what makes interrupts safe across restarts and long delays.
Resume after an interrupt¶
To continue a paused workflow, provide an InterruptResponse:
var result = await runner.ResumeWithResponseAsync(
workflow,
runId: "run-abc",
interruptResponse: InterruptResponse.ApprovedResponse(
respondedBy: "alice",
comment: "Ship it"));
The runner loads the interrupted checkpoint and resumes execution.
How resume behaves¶
- for declarative interrupts, execution simply continues past the pause point
- for programmatic interrupts, the response is returned back to
context.InterruptAsync(...)
That is the key difference.
Interrupt requests and responses¶
Interrupts use two payload types:
InterruptRequest— what the workflow is asking forInterruptResponse— what the human or external system sends back
InterruptRequest¶
public sealed record InterruptRequest
{
public required string RunId { get; init; }
public required string WorkflowId { get; init; }
public required string NodeId { get; init; }
public string? Reason { get; init; }
public string? Title { get; init; }
public string? Description { get; init; }
public object? Payload { get; init; }
public IReadOnlyDictionary<string, object?> Metadata { get; init; }
}
InterruptResponse¶
public sealed record InterruptResponse
{
public required InterruptStatus Status { get; init; }
public bool Approved => Status == InterruptStatus.Approved;
public bool Rejected => Status == InterruptStatus.Rejected;
public bool TimedOut => Status == InterruptStatus.TimedOut;
public bool Cancelled => Status == InterruptStatus.Cancelled;
public string? RespondedBy { get; init; }
public string? Comment { get; init; }
public object? Payload { get; init; }
public DateTimeOffset RespondedAt { get; init; }
}
Factory helpers¶
InterruptResponse.ApprovedResponse(payload: data, respondedBy: "alice", comment: "Looks good");
InterruptResponse.RejectedResponse(respondedBy: "bob", comment: "Needs more testing");
InterruptResponse.TimedOutResponse(comment: "No response within 24 hours");
InterruptResponse.CancelledResponse(comment: "Workflow cancelled by admin");
Optional automation with IInterruptHandler¶
Interrupts do not always need a human.
You can register an IInterruptHandler to handle them automatically.
public interface IInterruptHandler
{
Task<InterruptResponse> HandleAsync(
InterruptRequest request, CancellationToken ct = default);
}
Register one like this:
If a handler is configured and returns a response, execution continues without pausing.
This is useful for:
- CI auto-approval
- routing to queues
- webhook-backed approval systems
- environment-specific automation
Example¶
public class CiAutoApproveHandler : IInterruptHandler
{
public Task<InterruptResponse> HandleAsync(InterruptRequest request, CancellationToken ct)
{
return Task.FromResult(InterruptResponse.ApprovedResponse(
respondedBy: "ci-pipeline",
comment: "Auto-approved in CI environment"));
}
}
Interrupts in multi-agent workflows¶
Interrupts show up naturally in multi-agent patterns.
Approval-gated handoffs¶
If an agent uses:
then a handoff request pauses for approval before the transfer happens.
Human escalation¶
If an agent uses:
then Spectra interrupts the workflow instead of failing or stopping silently.
See Guard Rails for the full multi-agent safety model.
A simple mental model¶
- declarative interrupt = "pause here"
- programmatic interrupt = "pause here if the step decides it should"
- resume = "continue with this response"
That is the core workflow.
What's next?¶
- Checkpointing
See how interrupted runs are persisted and resumed.
- Time Travel
Resume or fork from an interrupted checkpoint.
- Workflow Runner
Learn how the execution loop handles interrupts.