Back to blog
Reference
June 11, 202611 min read

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 project

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

The 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

Keep reading