Skip to content

Workflows

The workflow engine is the agent’s durable loop. An operator does not script tasks; the coordinator starts workflow runs, and each run advances one step at a time. A task is a unit of work the agent drives exclusively through the current step of its run.

  • A run is stored durably with its current step, status, retry count, and step_state.
  • Each step has a retry policy, a timeout, a poll interval, and optional mutexes.
  • Step timeout is the total wall-clock budget for one step across all attempts, including retry backoff. Schedule delay (waiting for the workflow’s parallelism limit) and worker-slot waits do not count against it.
  • Retry backoff is the wait between a failed attempt and the next; the first retry waits exactly the strategy’s base.
  • Mutexes such as deploy:staging and deploy:production serialize deploys. A stale mutex — one held by a run that is no longer running — is reconciled at startup and on the watchdog cadence, so it stops blocking within one watchdog interval instead of at the next restart.
  • A fail-fast step has a zero timeout: it times out on its first observed tick before its action runs, so an unstarted fail-fast step fails at queue time.

Retries are recorded as step transitions. A run that exhausts its retry budget produces one retry row per errored attempt, then a failed row.

KindRole
task_lifecycleIssue delivery: implement, CI, staging, merge, production
github_syncIssue pickup and external PR review
harness_convergeDiff the repo against the merged harness pack, open one sf/harness PR
docs_convergeDaily docs contract scan; a coding-runtime worker fixes drift and the agent opens one sf/docs PR
production_monitorPoll environment health; escalate to investigation on threshold
investigate_incidentsSpawn an investigator and file issues for findings
optimize_performanceObserve → hypothesize → open PR-gated performance issues
pentestPeriodic security audit (Cloudflare security-audit skill) of the repo source; files confirmed and needs-validation security issues
self_healTurn agent failures, red default-branch CI, and code-health findings into pickup issues (source profile)
landingSerialize landing: take the head of the per-repo landing queue and rebase, verify, and merge one PR at a time
backlog → waiting_for_info → waiting_for_worker → implementing
→ waiting_for_ci_checks → deploying_to_staging → staging_healthchecks
→ merging_pr → [waiting_for_rebase_worker / rebasing_from_main]
[waiting_for_ci_fix_worker / fixing_ci]
→ deploying_to_prod → prod_healthchecks → [reverting_pr] → done

delivery_profile=source short-circuits staging and production: once CI is green the run merges to main using pr_merge_method and is done. delivery_profile=live runs the full staging → production path. pr_merge_method (squash default, rebase/merge allowed) picks the merge method.

The workflow runs at most 3 in parallel and tolerates 5 failure recoveries.

  • A worker death with no observed result is a transient failure: the run probes the PR first. If the PR still needs that worker (CI failed, or the branch still needs a rebase) it returns to the matching waiting_for_* step and respawns a fresh sandbox, resuming the run’s Pi Coding session so the worker continues its conversation; if the worker’s goal was already achieved (its push turned CI green) it continues at waiting_for_ci_checks without spending a failure recovery.
  • A completed coding-runtime session that opened no PR also respawns rather than escalating on the first occurrence.
  • Transient GitHub API failures (error connecting to api.github.com, HTTP 5xx, rate limits) keep the CI steps pending and retry on the next poll instead of permanently failing the run. Auth, permission, and validation errors still fail.
  • Dead commit statuses do not park the CI wait: a legacy status (StatusContext — deploy-preview bots and the like) that has been pending past a 40-minute window is treated as a lost webhook, not a running check. When nothing else is non-green the PR counts as clean and the run proceeds. GitHub reports such PRs as UNSTABLE (mergeable; a non-required check is not green) and Steelframe still honors BLOCKED, which is how required checks or reviews surface. CheckRuns are never treated this way: Actions reports its own transitions and enforces its own job timeout.
  • A recorded non-transient error (for example, an API error) fails the run.
  • Triage can park a task: an epic or a request for clarification labels the issue steelframe:needs_human and stops the run. Parking is the intended outcome, not a defect, and the label holds pickup until a human responds. The park is durable: if the comment or label write fails, github_sync retries it and holds the issue out of pickup until the label lands.
  • waiting_for_info resumes on a human reply or a Decision comment (<!-- steelframe-decision -->, including GitHub App authorship). If steelframe:needs_human delete fails after the comment, github_sync retries pending unpark (does not hold pickup).
  • If a run still fails, the issue may be labeled steelframe:needs_human; failure is terminal by design. Start again from a new issue or run.

Step errors and failure recoveries are appended to the run’s step log, viewable in the run detail.

Discord carries alerts and their resolutions only; routine operations stay in the run view and the journal.

  • ⚠️ #N needs a human — a task stopped on something only a human can clear (a real blocker; the issue is labelled steelframe:needs_human).
  • 🔧 #N stopped on agent infrastructure — the host, not the task: missing or outdated CLI, full disk, exec format. The issue is not parked.
  • ▶️ #N resumed — the resolution of either alert: needs_human cleared, or a failed run retried.
  • Environment degraded / healthy again, and background-workflow failures (debounced per workflow).

Not sent: successful task completions, and idle memory recycles. Both stay log-only — journalctl -u steelframe carries agent memory every five minutes and recycling idle agent when the guard trips.

MethodPathPurpose
GET/api/workflowsSummary counts by kind and status
GET/api/workflows/{kind}Runs of one kind (?limit= &offset=)
GET/api/workflows/{kind}/runs/{id}Run plus step log
POST/api/workflows/{kind}/runs/{id}/terminateTerminate a running run
POST/api/workflows/optimize/triggerStart or report an optimize_performance run
POST/api/tasks/syncForce a github_sync run

Termination sets the run’s error to terminated by @<user> via API/TUI, records workflow.terminate, and returns { "terminated": true }. Terminating a run that is not running returns terminated: false with a reason.

  • Singleton workflows (github_sync, harness_converge, docs_converge, self_heal, optimize_performance, pentest, production_monitor, landing) have max parallelism 1; their cadence keys are minutes or hours, and 0 disables them. landing is continuous (it waits on an empty queue), so it has no cadence key.
  • delivery_profile=source never starts production_monitor and never starts self_heal when the profile is live.
  • A task_lifecycle run at merging_pr (source) or deploying_to_staging with staging_deploy_strategy=merge_pr (live) waits on the landing queue: repo_merge_strategy=steelframe_landing (default) serializes the merge, github_merge_queue bypasses the queue and auto-merges as before.
  • A run terminated or failed after partial work leaves its sandbox cleaned up, not resumable.