Skip to content

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.

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 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

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).

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.

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.

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]

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

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]
  • 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>" and source_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’s source_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 ref on hooks: ref: on a hook, or a hook whose action is a type: task action carrying ref, is rejected by stroem validate. At runtime such a hook is not fired, and the error is logged as a server event on the original job.

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, and on_cancel are 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, or restart — a user clicking Restart from a step; see Re-running and Restarting Jobs). Child jobs from type: task actions do not trigger workspace hooks.
  • If multiple YAML files define workspace-level hooks, they are merged (extended, not replaced).
  • The same hook.* template context and secret.* variables are available as in task-level hooks.

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 }}"

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 }}"