Hooks
Tasks can define on_success, on_error, and on_cancel hooks that fire automatically when a job reaches a terminal state. Each hook references an existing action and can pass input rendered with job context.
Syntax
Section titled “Syntax”tasks: deploy: flow: build: action: build-app deploy: action: deploy-app depends_on: [build] on_success: - action: notify-slack input: webhook_url: "{{ secret.SLACK_WEBHOOK_URL }}" message: "Deploy {{ hook.task_name }} succeeded ({{ hook.duration_secs }}s)" on_error: - action: notify-slack input: webhook_url: "{{ secret.SLACK_WEBHOOK_URL }}" message: "Deploy {{ hook.task_name }} FAILED: {{ hook.error_message }}"Hook context variables
Section titled “Hook context variables”Hook input values are Tera templates with access to a hook object containing job context, plus workspace secrets under the secret key:
| Variable | Type | Description |
|---|---|---|
hook.workspace |
string | Workspace name |
hook.task_name |
string | Task that completed/failed |
hook.job_id |
string | UUID of the original job |
hook.status |
string | "completed", "failed", or "cancelled" |
hook.is_success |
bool | true if completed |
hook.error_message |
string/null | All failed step errors combined |
hook.source_type |
string | Original job source ("api", "trigger", etc.) |
hook.source_id |
string/null | Original job source ID |
hook.started_at |
string/null | ISO 8601 timestamp |
hook.completed_at |
string/null | ISO 8601 timestamp |
hook.duration_secs |
number/null | Execution duration in seconds |
hook.failed_steps |
array | Failed step details (see below) |
hook.revision |
string/null | Workspace revision (git SHA or folder hash) pinned on the job |
hook.ref |
string/null | The git ref the job runs at, as written; null unless it is a pinned job |
Each entry in hook.failed_steps contains:
| Field | Type | Description |
|---|---|---|
step_name |
string | Name of the failed step |
action_name |
string | Action that was executed |
error_message |
string/null | The step’s error message |
continue_on_failure |
bool | Whether this row’s own flow step has continue_on_failure set. A loop instance row reports its placeholder’s flag |
tolerated |
bool | Same value as continue_on_failure above — this failure does not by itself fail the job; other untolerated failures or a cancellation can still decide the outcome |
carried_over |
bool | true if this failure was carried forward from the source run by a job restart rather than produced by this job |
on_cancel hooks
Section titled “on_cancel hooks”Tasks can define on_cancel hooks that fire when a job is cancelled — whether manually via the API, by a timeout, or by a scheduler concurrency policy (cancel_previous).
Syntax
Section titled “Syntax”tasks: deploy: flow: build: action: build-app deploy: action: deploy-app depends_on: [build] on_cancel: - action: notify-slack input: webhook_url: "{{ secret.SLACK_WEBHOOK_URL }}" message: "Deploy {{ hook.task_name }} was cancelled"on_cancel hooks use the same hook.* template context as on_success / on_error, with hook.status set to "cancelled" and hook.is_success set to false. If no on_cancel hooks are defined, nothing fires for cancelled jobs.
on_suspended hooks
Section titled “on_suspended hooks”Tasks can define on_suspended hooks that fire when any step in the task enters suspended state, typically from an approval gate. Use this to notify approvers or escalate when human action is required.
Syntax
Section titled “Syntax”tasks: deploy-production: on_suspended: - action: notify-approvers input: channel: "#deploy-approvals" message: "{{ hook.task_name }} in {{ hook.workspace }} is awaiting approval" flow: approve: type: approval message: "Approve production deployment?" deploy: action: deploy-script depends_on: [approve]Available variables
Section titled “Available variables”on_suspended hooks get a hook object describing the suspended step:
| Variable | Type | Description |
|---|---|---|
hook.workspace |
string | Workspace name |
hook.task_name |
string | Task containing the suspended step |
hook.job_id |
string | UUID of the job |
hook.step_name |
string | Name of the suspended approval step |
hook.message |
string | Rendered approval message shown to approvers |
hook.source_type |
string | Original job source ("api", "trigger", etc.) |
hook.source_id |
string/null | Original job source ID |
hook.revision |
string/null | Workspace revision (git SHA or folder hash) pinned on the job |
hook.ref |
string/null | The git ref the job runs at, as written; null unless it is a pinned job |
Practical examples
Section titled “Practical examples”Slack notification with actionable message:
actions: notify-slack-approval: type: script script: | curl -X POST "$WEBHOOK_URL" \ -H 'Content-Type: application/json' \ -d '{ "text": "{{ input.message }}", "blocks": [ { "type": "section", "text": {"type": "mrkdwn", "text": "{{ input.message }}"} }, { "type": "context", "elements": [{"type": "mrkdwn", "text": "Job ID: {{ input.job_id }}"}] } ] }' input: message: { type: string } job_id: { type: string }
tasks: deploy: on_suspended: - action: notify-slack-approval input: message: | *Approval needed for {{ hook.task_name }}* Workspace: {{ hook.workspace }} Job: {{ hook.job_id }} job_id: "{{ hook.job_id }}" flow: approve: type: approval message: "Deploy {{ input.service }} to production?" deploy: action: deploy-app depends_on: [approve]PagerDuty escalation:
tasks: critical-maintenance: on_suspended: - action: create-pagerduty-incident input: title: "{{ hook.task_name }} awaiting approval in {{ hook.workspace }}" service_id: "{{ secret.pagerduty_service_id }}" job_id: "{{ hook.job_id }}" flow: approve: type: approval message: "Proceed with critical maintenance?" timeout: 1h execute: action: maintenance-script depends_on: [approve]Behavior
Section titled “Behavior”- Fire-and-forget: Hook job creation is best-effort. Failures are logged but never affect the original job.
- No recursion: Jobs created by hooks (
source_type = "hook") never trigger further hooks. - Visible as jobs: Hook jobs appear in the job list with
task_name = "_hook:<action>"andsource_type = "hook". A hook job’s detail page links back to the job that fired it (hook for<id>under the title); over the API that id is the hook job’ssource_job_id. - Normal execution: Hook jobs go through the normal claim/execute flow on workers.
- Multiple hooks: You can define multiple hooks per event. They all fire independently.
- Validation: Hook action references are validated at parse time — referencing a non-existent action is an error.
- Failure visibility: If a hook job fails at runtime, the failure is logged as a server event on the original job (visible in the “Server Events” panel on the job detail page).
- Pinned jobs: The hooks of a job that runs on a git ref come from that job’s commit, and the hook job runs at the same ref and commit.
- No
refon hooks:ref:on a hook, or a hook whose action is atype: taskaction carryingref, is rejected bystroem validate. At runtime such a hook is not fired, and the error is logged as a server event on the original job.
Workspace-level hooks
Section titled “Workspace-level hooks”You can define on_success, on_error, and on_cancel hooks at the top level of your workflow YAML. These act as fallback defaults: they fire only when the specific task has no hooks defined for that event type.
actions: notify-slack: type: script script: 'curl -X POST "$WEBHOOK_URL" -d "{\"text\": \"$MESSAGE\"}"' input: message: type: string
on_success: - action: notify-slack input: message: "Job {{ hook.task_name }} in {{ hook.workspace }} completed successfully"
on_error: - action: notify-slack input: message: "Job {{ hook.task_name }} in {{ hook.workspace }} FAILED: {{ hook.error_message }}"
tasks: deploy: flow: step1: action: deploy-app # No on_success/on_error here — workspace-level hooks fire
backup: flow: step1: action: backup-db on_error: - action: notify-slack input: message: "Backup failed — page oncall!" # Task-level on_error takes precedence; workspace on_error is skipped # Workspace on_success still fires (because task has no on_success)on_success,on_error, andon_cancelare evaluated independently. A task can override one while inheriting the others.- Workspace hooks only fire for top-level jobs (source type
api— programmatic calls,user— authenticated API calls,trigger— cron triggers,webhook— webhook triggers,mcp— MCP tool invocations,retry— server-initiated retries,rerun— a user clicking Re-run on a past job, orrestart— a user clicking Restart from a step; see Re-running and Restarting Jobs). Child jobs fromtype: taskactions do not trigger workspace hooks. - If multiple YAML files define workspace-level hooks, they are merged (extended, not replaced).
- The same
hook.*template context andsecret.*variables are available as in task-level hooks.
Cleanup and notification after a failure
Section titled “Cleanup and notification after a failure”A dependent’s own accept: [completed, failed] (or accept: any) does let it run after an upstream failure, and that’s fully decoupled from whether the job still fails (continue_on_failure, self-scoped on the upstream step — see Conditionals). But a step that isn’t really part of the flow’s own DAG — cleanup, notifying on-call, tearing down partial resources — still belongs in an on_error (or on_cancel) hook, so it fires exactly once on any terminal outcome without wiring an accept edge to every step that might fail:
tasks: deploy: flow: deploy: action: deploy-app on_error: - action: run-cleanup input: env: "{{ hook.workspace }}"Task actions in hooks
Section titled “Task actions in hooks”Hook actions can be type: task, creating a full job for the referenced task instead of a single-step hook job. It is still a hook job (source_type = "hook"), not a child of the job that fired it — so the task keeps its own retry: and is not cancelled along with that job:
tasks: deploy: flow: deploy: action: deploy-app on_error: - action: run-cleanup input: env: "{{ hook.workspace }}"