The workflow file, in detail: tokens, conditions, outputs and inheritance
A manual-style reference for .agentis/workflows: which file runs when, what [%TOKEN%]s and env variables each step gets, the exact if-condition grammar, output types and extends merge rules.
Jakub Černý
Platform
This is the companion reference to the workflows introduction: less philosophy, more contract. Everything below describes the behavior of workflow files under .agentis/workflows/ as the adapter actually executes them.
Which file runs, and when it is read
- default.yaml — a regular task run, in a dedicated worktree on a task branch. The only workflow whose followups section is honored.
- project.yaml — runs when the task has project scope. Executes directly in the project directory, with no worktree and no git operations.
- merge.yaml, close.yaml, <name>.yaml — named workflows started explicitly, typically by followup buttons. A missing file is a start error, not a silent fallback.
- _base.yaml — shared parent for extends. It has no steps and cannot be started on its own.
The file is loaded, extends is resolved, tokens are interpolated, and the result is frozen once at the start of the run. Editing the file in the worktree while the workflow runs has no effect — the running workflow keeps its snapshot.
Run files live in two places. A regular task run writes prompt.md, context.json and step outputs under <worktree>/.agentis/runs/<attempt>/, and step output paths resolve relative to the worktree. Project scope and named workflows write under <project_run_root>/<run_id>/<attempt>/ outside the worktree — deliberately, because workflows like merge and close may delete the worktree they run against.
Token reference
String values anywhere in the YAML may contain [%TOKEN%] placeholders, replaced at load time. A token outside this list is a load error; a known token with no value becomes an empty string. Every token is also exported to each step as an environment variable of the same name.
NAMESPACE Kubernetes namespace of the run
WORKDIR absolute path of the worktree
RUN_DIR run files directory (prompt.md, context.json, outputs)
MAIN_DIR main project directory (context.working_dir)
RUN_ID run identifier
TASK_ID task identifier
TASK_NUMBER human task number
TASK_TITLE task title
BRANCH task branch
BASE_BRANCH target branch
GITHUB_REPO GitHub repository of the projectOn top of the tokens, the adapter injects AGENTIS_RUN_ID, AGENTIS_TASK_ID, AGENTIS_RUN_DIR, AGENTIS_PROMPT_FILE and AGENTIS_CONTEXT_FILE into every step, plus AGENTIS_SESSION_ID, AGENTIS_MODEL, AGENTIS_AGENT and AGENTIS_EFFORT when the task context provides them — and every var output of steps that already finished. Both executors wrap each step in the same bash prologue: set -euo pipefail, source the configured envFiles, cd into the step workingDir (falling back to the workflow workingDir, then $WORKDIR).
The if grammar, precisely
condition := and ( '||' and )*
and := term ( '&&' term )*
term := VAR | '!' VAR | VAR '==' value | VAR '!=' value
MODE == production && !DRY_RUN
GITHUB_REPO && ENV_READY != 'true'
LABEL == 'a && b' # quote values containing spaces or && / ||- Variables are the var outputs of previous steps plus the built-in token values. On a name collision the var output wins, so a step can override a built-in for the rest of the run.
- && binds tighter than || — A && B || C means (A && B) || C. There are no parentheses.
- Negation applies only to a bare variable, never to a comparison or a group.
- An unknown variable is an empty string. A bare VAR test treats "", 0, false and no (case-insensitive) as falsy.
- Condition syntax is validated when the file loads, not when the step runs. A skipped step is reported to the timeline as skipped, with the condition attached, and its outputs are never applied.
Output types and when they apply
Outputs are files the step writes, declared with paths relative to the output root (the worktree for task runs, the run directory otherwise); a path escaping that root is rejected. Type var is read immediately after the step finishes, so later steps can use it. Every other type is collected and applied to Agentis in a single call after the workflow completes.
agent_comment bodyFrom, status completion comment body + target task status
session_id valueFrom stored on the run for later resume
url / text label, valueFrom attachment on the comment (link / text)
artifact name, path file attached to the comment (base64)
var name, valueFrom variable for if conditions and step envThe rule that surprises people: outputs of successful steps are applied even when the workflow as a whole fails. Outputs of skipped and failed steps are not — including steps that failed with continueOnError. If an output did not reach the task, check exactly these cases first: missing or empty file, step skipped, step failed, or a path outside the output root.
Error handling: three flags, three meanings
- continueOnError: true — the failure is reported but does not stop the workflow. The step counts as failed: its var outputs are not read and its other outputs are not applied.
- retries: N — up to N repeats without backoff, so at most N + 1 executions. Only the final result is reported, with the attempt count; each retry gets a unique job name (<job>-r<n>) because the failed Kubernetes Job still exists under the original one.
- always: true — the step runs even after an earlier step failed fatally, in original order, with if conditions still honored. It receives AGENTIS_WORKFLOW_STATUS (success/failed) and AGENTIS_FAILED_STEP, which is how cleanup steps decide to compose a failure comment.
A fatal failure without these flags ends the workflow: the timeline gets a failed step event with the last ~50 log lines, the remaining non-always steps are reported as skipped, and the run closes with idle failed naming the step. Note that followups are never offered after a failed run, even if an always step delivered a comment.
extends: one level, field-by-field
A top-level extends: _base loads .agentis/workflows/_base.yaml as the parent and merges raw YAML before validation, which means schema defaults in the child never override explicit parent values, and token interpolation runs after the merge — tokens written in the parent resolve in the child run’s context. Exactly one level is supported: a parent with its own extends is an error, chained or cyclic alike.
- Scalars (image, workingDir, timeoutSeconds, …) — the child overrides; absent in the child, the parent value stands.
- env — merged per key, child wins.
- envFiles, volumeMounts, imagePullSecrets, volumes — concatenated parent + child; a mapping item with the same name replaces the parent item in place, an exact duplicate is dropped. Children typically only add mounts, so replacement semantics would force copying the whole base block.
- steps and followups — never inherited. Steps are the substance of a workflow; “inherit and patch a step list” has no readable semantics, so each child declares its own.
Strictness as a feature
The schema rejects unknown keys, unknown tokens and malformed conditions at load time, before any step runs. A typo in continueOnError fails the start instead of silently doing nothing on step nine. When a workflow refuses to start, the error names the exact contract violated: a missing image for the kubernetes executor, a missing workflow file, an extends target that does not exist, or a second run on a task that is still busy.
See Agentis in action on your own task
Sign in, describe a task and watch an agent deliver reviewed, ready-to-ship work in minutes.
Try Agentis now