Running an engagement

Live progress, the event stream, and what the console shows while a run is in flight.

A submitted engagement goes onto a queue, waits for the scheduler to pick it up, and then streams events to the engagement page until it ends.

The queue is sequential

Submitting writes a job onto a Redis list. One process — the scheduler on the agent host — polls that list, and the poll loop takes one job, awaits the whole run, and only then looks for the next. There is no priority and no preemption. Five queued runs are five runs one after another.

The plan tier of the user who created a schedule caps how many engagements the organization may have queued or running: free 1, pro 3, enterprise 10. An unrecognised tier gets the free allowance rather than an unlimited one. The cap governs how much may be in flight, not how fast the queue drains, and it is checked only when a schedule fires: if the organization is at its limit, the occurrence is skipped and the reason is recorded against the schedule. A manually submitted engagement is not refused for the cap. Caps and skips describes the cap and the skip, and Plans and limits the tier.

The event stream

The engagement page opens a server-sent-events connection to /api/engagements/{id}/progress. Events are not read from a live channel: the scheduler appends each one to a per-engagement list in Redis, and the endpoint reads that list by cursor, replaying everything recorded so far and then polling for more once a second.

Two consequences follow. Reconnecting replays the run from its first event rather than resuming where you were, so closing the tab during a run loses nothing — as long as the run is still going when you reopen the page. If it ended meanwhile, the console will not open the stream at all, and the trace has to be fetched from the endpoint directly. And the trace is kept for 24 hours and capped at 5,000 events per engagement; past either limit the oldest events are gone, though the findings, artifacts and report are not affected.

The endpoint checks that the engagement belongs to your organization. Another organization's engagement returns 404 rather than 403.

What an event carries

Events the agent emits carry a type, the phase they came from, a human-readable message, the engagement id and a UTC timestamp. Three further fields appear only where they apply: command on a command event, severity on a finding event, and tool.

The scheduler's own events are assembled separately and are thinner: a type, a phase and a message, with no engagement id or timestamp of their own. engagement_sealed is the exception in the other direction — it carries the seal values in place of a message.

Type Emitted when
job_started the scheduler is launching the container
phase_change the run starts, a phase completes, a phase begins, or the rules-of-engagement document is rendered
tool_execution a command has passed the scope guard and is about to run
command_blocked the scope guard refused a command, or a tool call's arguments would not parse
finding a finding was recorded, with its title and severity
error a step raised, carrying the consecutive-failure count; the abort that follows the fifth failure is one more error event — the sixth — carrying no count
artifact_captured an artifact was hashed and stored, with its hash, kind, size and sequence
engagement_complete the agent finished its loop; the scheduler emits a second one after persisting results
engagement_failed the container exited non-zero, produced no results, or the scheduler itself errored
engagement_sealed the run was sealed, with the chain head, manifest hash, entry count and key id

The stream carries the command text the agent runs. It does not carry command output: stdout and stderr go into the artifact, not into the event.

What the console renders

The engagement page opens the stream while the status is queued or running, and not for a run that has already ended. Until the container starts there is nothing in the trace, and the terminal shows waiting for events….

Two things are drawn. A six-stage pipeline — intel, threat model, vuln scan, exploit, post-exploit, report — highlights the stage matching the most recent phase value. Pre-engagement is not one of the six, because it renders the rules-of-engagement document and advances without a model call. Below it, a terminal renders phase_change, tool_execution, command_blocked, finding, error and the two terminal events as colored lines.

artifact_captured and engagement_sealed are published to the stream but the console does not render them. Evidence and seal details are read from the engagement afterwards rather than watched live.

The console stops at the first engagement_complete or engagement_failed and closes the connection. The agent emits engagement_complete itself, as its last act before the container exits, so the work that follows — writing findings to the database, sealing, and the scheduler's own completion event — happens after the terminal has stopped. On engagement_complete the page refreshes findings and requests a report; on engagement_failed it marks the engagement failed.

What you cannot do

There is no pause, no abort and no way to steer a run in flight. An engagement that has started ends on its own terms. The ways it can end are in Lifecycle and When a run fails.

Last updated 2026-09-23