Skip to content

Agent Workflow Language (AWL) Specification

Overview

The Agent Workflow Language (AWL) is a lightweight scripting language used to orchestrate AI agent workflows.

AWL focuses on intent-driven orchestration rather than explicit tool invocation.

Instead of specifying which tools to call, AWL scripts define:

  • goals
  • workflow structure
  • conditions
  • loops
  • variable propagation

The agent remains responsible for selecting tools and strategies to accomplish each task.

This design keeps AWL:

  • human readable
  • LLM-friendly
  • flexible
  • safe

Design Principles

Intent-driven workflows

Scripts describe what must be achieved, not how to implement it.

Agent autonomy

The agent determines:

  • which tools to use
  • how to search or analyze
  • when a task is complete

Minimal syntax

The language remains simple so that both humans and LLMs can easily produce and edit scripts.

Deterministic structure

Although the agent reasons dynamically, the workflow structure is deterministic and validated by the runtime.


Script Structure

An AWL script begins with @start and ends with @end.

Example:

@start

@task find_server Goal: Find where the HTTP server is initialized. Success: Identify the file and function responsible for startup. Expose: server_file, server_function @end

@end


Directives

AWL defines the following directives.

Directive Description
@start start workflow
@end end block
@task define agent task
@set assign variable
@if conditional execution
@else alternate branch
@loop iterate collection
@continue skip current loop iteration
@break exit loop early
@return return workflow result
@goal autonomous goal pursuit

Task Hints

Tasks may include execution hints placed after the task name.

Hints influence the context available to the agent, but do not prescribe specific tools.

Example:

@task fresh_search @no-history @no-kg Goal: Re-evaluate where the HTTP server is initialized. Success: Identify the startup path using only repository evidence. Expose: server_file, server_function @end


Supported Hints

@no-history

The agent must ignore prior conversation history and previous reasoning context.

This ensures that the task is evaluated with a fresh reasoning context.

Example:

@task fresh_analysis @no-history Goal: Analyze the repository structure without using previous conclusions. @end


@no-kg

The agent must not consult the external knowledge graph or knowledge base.

Only live data sources such as repository inspection or runtime tools may be used.

Example:

@task repository_only @no-kg Goal: Determine how the system initializes using only source code evidence. @end


max_tool_calls=N and max_time=N

max_tool_calls overrides the default tool call budget (100) for a single task. The budget is soft: when it is reached, the agent is told to wrap up and gets up to 50% more tool calls.

max_time sets a wall-clock budget, in seconds, for a single task. Without it, a task has no time limit (only a stalled model connection is cut off).

When either budget runs out, the agent is not cut off mid-work: it gets one final turn in which it can only call internal__finish_task, to report success with what it has or failure with what is missing.

Use this when a task may need many tool calls, such as running a detailed analysis before creating a ticket.

Example:

@task analyze_and_report max_tool_calls=200 max_time=1800 Goal: Run a root cause analysis on the job and create a Jira ticket. Expose: jira_ticket @end


model=\<level>

model selects an abstract capability level for a single task, so scripts stay provider-neutral. The valid values are low, medium, high, and default. Use a lower level for cheap/mechanical work and a higher level for deep analysis. A task with no model= uses default (the session model).

Each level maps to a concrete model via env vars:

level env var fallback when unset
low AI_ASSIST_MODEL_LOW default
medium AI_ASSIST_MODEL_MEDIUM default
high AI_ASSIST_MODEL_HIGH default
default AI_ASSIST_MODEL high, then claude-sonnet-4-6

default is the session model — the same one used for interactive sessions and set by --model. default and high fall back to each other: set only AI_ASSIST_MODEL and high tracks it; set only AI_ASSIST_MODEL_HIGH and the session model tracks that (so a config that defines only the AWL levels still runs on a sensible model). Set both explicitly to make them differ.

Model environment values may include an effort suffix, for example AI_ASSIST_MODEL=claude-sonnet-4-6:medium or AI_ASSIST_MODEL_HIGH=claude-opus-4-6:max. A recognized suffix (low, medium, high, xhigh, max, or no) is removed from the model ID and becomes that model's effort; no means send no effort, for a model that rejects the parameter. A tier (AI_ASSIST_MODEL_LOW/_MEDIUM/_HIGH) without its own suffix inherits the session effort; :no opts it out. Explicit AI_ASSIST_EFFORT and --effort values take precedence. Unrecognized suffixes are preserved as part of the model ID for custom providers.

The value is validated at workflow load time. Any value other than low, medium, high, or default (including a concrete model id) fails the workflow before any task executes.

Example:

@task search_jobs model=low @no-kg Goal: Search DCI for failed jobs in the last 24 hours. Expose: failed_jobs @end

@task root_cause model=high Goal: Perform root cause analysis on ${failed_jobs}. Expose: rca_result @end

effort=

An optional effort attribute overrides the configured request effort for one task. Valid values are low, medium, high, xhigh, max, and no (send no effort); availability depends on the selected model and endpoint. A task without effort= inherits its tier's effort suffix, or the session effort (AI_ASSIST_EFFORT), or the provider default when unset. Effort does not change the model's thinking mode.

@task quick_lookup effort=low
Goal: Find the matching job.
@end

@task investigate effort=high
Goal: Explain the root cause.
@end

@continue-on-failure

By default, if a top-level task (outside a loop) fails, the workflow stops immediately and returns success: false. This prevents cascading errors from missing variables.

Tasks inside loops are not affected — loop iterations that fail are skipped, and the loop continues to the next item.

The @continue-on-failure hint overrides the default fail-fast behavior, allowing the workflow to continue even if the task fails.

Example:

@task optional_cleanup @continue-on-failure Goal: Clean up temporary files. Not critical if this fails. @end


Tasks

Tasks represent objectives for the agent.

Syntax:

@task [hints...] [max_tool_calls=N] [max_time=N] [model=] [effort=] ... @end

Example:

@task locate_config Goal: Find where the server configuration is loaded. Context: Focus on startup code and configuration parsing. Success: Identify the entrypoint and parser. Expose: config_entrypoint, config_parser @end


Task Fields

Inside a task block the following fields may appear.

Field Purpose
Goal task objective
Context additional context
Constraints optional limitations
Success completion criteria
Expose variables produced

Shell Commands in Tasks

When a task requires the agent to run a specific shell command, enclose it in backticks:

@task update_repo Goal: Fetch the latest changes using git -C /path/to/repo fetch origin master. @end

Commands in backticks are auto-authorized by the runtime: they are temporarily added to the allowed commands list for the duration of the workflow, so the user is not prompted for confirmation each time.

This only applies to scripts the user started: ai-assist /run, and actions (internal__create_action) whose AWL prompt the user confirmed. When the agent runs a script itself (introspection__execute_awl_script), its commands go through the normal allowlist and confirmation.

Commands are taken from the script text, never from interpolated values. A ${var} may only choose a directory, with the script name written literally (${tools_dir}/build.sh), and its value must be a plain path (no spaces or shell syntax).

Commands written without backticks will require manual authorization in interactive mode.


Variables

Variables can be defined using @set.

Example:

@set target = "HTTP server"

Variables can be referenced using interpolation:

${target}

Example:

Goal: Find where ${target} is initialized.

Initial Variables

Variables can be injected before the script runs, without needing @set.

From the CLI:

ai-assist /run workflow.awl target="HTTP server" days=7

From an agent prompt: ask the agent to run a .awl file and it will call the introspection__execute_awl_script tool with the variables you specify.

Injected variables are available from the first node, just like @set variables.

Built-in Variables

AWL automatically injects built-in variables into every execution. These are available without any CLI arguments or @set directives.

Date/time (generated fresh per execution):

Variable Example Description
date 2026-07-17 Local date
time 14:30:05 Local time
datetime 2026-07-17 14:30:05 Local date+time
utc_date 2026-07-17 UTC date
utc_time 12:30:05 UTC time
utc_datetime 2026-07-17 12:30:05 UTC date+time
timezone CEST Local timezone abbreviation

Paths:

Variable Description
config_dir Config directory (~/.ai-assist)
reports_dir Reports directory (~/ai-assist/reports)
logs_dir Logs directory (~/.ai-assist/logs)
tmpdir Session-scoped temporary directory (created at startup, cleaned up on exit)

User-Defined Variables (variables.yaml)

Define custom variables in ~/.ai-assist/variables.yaml to make them available in all AWL scripts. This makes scripts location-independent — different users can run the same script with their own settings.

workspace: ~/ai-assist/workspace
team: dci
jira_project: CILAB

Values support ~ expansion and $ENV_VAR substitution. The file is optional.

Precedence (highest wins):

  1. CLI / caller-provided variables (ai-assist /run script.awl key=value)
  2. User-defined variables from variables.yaml
  3. Built-in variables (date/time, paths)

Variable Validation

AWL performs static analysis before execution to detect variables that are used before they are defined. This catches common mistakes such as:

  • Typos in variable names (e.g., @if failure_count > 0 when only failed_jobs was exposed)
  • Referencing a collect variable inside the loop that produces it (the collect variable is only available after the loop)
  • Using ${var} in a task goal when no prior task exposed that variable

Validation warnings are reported in three places:

  1. inspect_awl_script — included as variable_warnings in the result
  2. execute_awl_script — blocks execution and returns an error
  3. Runtime — printed to the console before execution starts

Scoping rules

Source Available where
@set All subsequent nodes
Expose: All subsequent nodes
@loop ... as item Inside the loop body
collect=var After the loop (not inside the body)
Initial variables Everywhere

Failure Handling

A task fails when its model call fails (API error such as a rate limit, or a stalled model stream): the error is never taken as the task's answer. Like any task failure, this stops the workflow unless the task has @continue-on-failure.

Reporting the task outcome

During a task, and only then, the agent has an internal__finish_task tool. The task prompt asks it to call the tool once when done:

  • status="success" with values holding the task's Expose: variables. These are taken as is (method finish_tool); only keys missing from values go through the usual extraction (JSON block in the reply, nudge, report sections).
  • status="failed" with a reason when the goal could not be achieved (a required step failed, a tool returned an error, needed data is missing). The task then fails with that reason, exactly like any other task failure, instead of exposing empty values that later steps would take as real results.

When a task runs out of its turn budget (max_tool_calls, after the extra wrap-up turns) or its max_time, it gets a final turn in which only internal__finish_task is offered. It fails as unfinished ("stopped before finishing") only if it still doesn't report.

If the agent ends its turn without calling the tool (typically narrating a next step, like "Now writing the report.", and stopping), it is nudged once to continue or report. If it still doesn't call it, the task behaves as before: success, with values extracted from its reply. A value that isn't in the reply stays unset (None) rather than being guessed.

@fail

Aborts the workflow immediately with an error message.

Syntax:

@fail

Example — abort unconditionally:

@fail Jira server is in maintenance

Example — abort when a task did not produce the expected result:

@if not jira_report @fail Jira collection failed - aborting quarterly review @end

The error is reported to the caller, with ${var} references in the message interpolated. Combine with @if/@else for conditional abort vs. alternative path.

Agent guidance: When a task's upstream service is unavailable or returns an error, do NOT expose the expected variables — leave them unset. The script can then detect the failure with @if not <variable> and abort with @fail.


@continue

Skips the rest of the current loop iteration and proceeds to the next item (like Python's continue). Only valid inside @loop or @while blocks. Outside a loop, behaves like @fail.

Syntax:

@continue

The message is required and supports ${variable} interpolation. It is logged for debugging. When used inside a @loop with collect=, skipped iterations contribute nothing to the collected results.

Example — skip a loop iteration when a task returns unexpected state:

@loop pull_requests as pr @task checkout_branch goal: Check out the branch for PR ${pr.number}. Expose: branch_ready @end @if branch_ready == 'unknown' @continue checkout nudged -- branch state unknown, skipping PR ${pr.number} @end @task analyze_pr goal: Analyze the PR. @end @end


@break

Exits the enclosing loop immediately (like Python's break). Only valid inside @loop or @while blocks. Outside a loop, behaves like @fail.

Syntax:

@break

The message is required and supports ${variable} interpolation. When used inside a @loop with collect=, items collected before @break are preserved.

Example — stop processing after finding the first match:

@loop candidates as candidate @task evaluate goal: Evaluate ${candidate}. Expose: score @end @if score > 90 @set winner ${candidate} @break found a winner with score ${score} @end @end

Nested loops

@continue and @break only affect the innermost enclosing loop (same semantics as Python). To exit an outer loop from inside a nested loop, use a variable:

@set done = false @loop repos as repo @if not done @loop branches as branch @task check Goal: Check ${repo} ${branch}. Expose: found @end @if found @set done = true @break found target in ${repo}/${branch} @end @end @end @end


Conditional Execution

Syntax:

@if ... @else ... @end

Example:

@if handlers

@task analyze_handlers Goal: Understand the detected handlers. Expose: handler_summaries @end

@else

@task search_handlers Goal: Search more broadly for request entry points. Expose: handlers @end

@end


Expressions

Expressions are intentionally simple.

Supported operations:

Expression Example
variable handlers
negation not report_exists
property config.entrypoint
index handlers[0]
length len(handlers)
comparison len(handlers) > 0
string literal status == 'unknown'

String literals use single or double quotes ('value' or "value"). Smart/curly quotes are automatically normalized to straight quotes. Comparison operators: ==, !=, >, <, >=, <=.


Loops

Loops iterate over collections.

Syntax:

@loop as [limit=N] [collect=[()]] ... @end

The optional limit=N parameter caps the number of iterations.

Collecting Results

The optional collect=<var> parameter accumulates exposed variables from each successful iteration into a list.

Example:

@loop handlers as handler limit=5 collect=summaries

@task inspect_handler Goal: Understand what ${handler} does. Expose: handler_summary, handler_priority @end

@end

After the loop, summaries contains:

[ {"handler_summary": "...", "handler_priority": "high"}, {"handler_summary": "...", "handler_priority": "low"} ]

To collect only specific fields, use parentheses:

@loop handlers as handler collect=summaries(handler_summary)

This produces:

[ {"handler_summary": "..."}, {"handler_summary": "..."} ]

Failed iterations are skipped and do not appear in the collected list.

Important: The collect variable is only available after the loop completes. Referencing it inside the loop body (e.g., ${summaries} in a task goal) will trigger a validation error, because the variable has not been populated yet.

Basic Loop

Example without collect:

@loop handlers as handler limit=5

@task inspect_handler Goal: Understand what ${handler} does. Expose: handler_summary @end

@end


Conditional Loop

@while repeats its body while an expression is truthy.

Syntax:

@while [max_iterations=N] ... @end

The optional max_iterations=N parameter limits the number of iterations (default: 100) as a safety guard against infinite loops.

Example:

@while running_count > 0 max_iterations=24

@task check_status @no-history @no-kg Goal: Count running DCI jobs. Do NOT fetch details. Expose: running_count @end

@end

Tasks inside @while do not count against the workflow's max_steps limit, since iteration count is already bounded by max_iterations.


Wait

@wait pauses execution for a specified duration.

Syntax:

@wait s|m|h

Examples:

@wait 30s @wait 5m @wait 1h

Useful for rate-limiting API calls or spacing out polling iterations.


Notify

@notify emits a user-facing message with variable interpolation. No agent call is made — this is a pure runtime operation.

Syntax:

@notify

Example:

@notify ${running_count} jobs still running @notify All daily DCI jobs have completed


Return

Workflows can return a result.

@return handlers


Polling Example

This workflow polls for running DCI jobs, notifies progress, and exits when done:

@start

@task check_jobs @no-history @no-kg Goal: Search for daily DCI jobs still running. Count them. Expose: running_count @end

@while running_count > 0 max_iterations=24

@notify ${running_count} DCI jobs still running

@wait 1h

@task recheck_jobs @no-history @no-kg @continue-on-failure Goal: Search for daily DCI jobs still running. Count them. Expose: running_count @end

@end

@notify All daily DCI jobs have completed

@end


Example Workflow

@start

@task find_handlers @no-kg Goal: Find HTTP handlers defined in the repository. Expose: handlers @end

@if len(handlers) > 0

@loop handlers as handler limit=5

@task inspect_handler @no-history Goal: Understand what ${handler} does. Expose: handler_summary @end

@end

@else

@task fallback_search Goal: Search more broadly for request entry points. Expose: handlers @end

@end

@return handlers

@end


Runtime Architecture

The runtime contains four main components.

Component Responsibility
Workflow Engine executes AWL scripts
Agent performs reasoning
Tool Runtime executes tools safely
Memory System stores workflow state

Execution pipeline:

AWL Script ↓ Parser ↓ Workflow AST ↓ Runtime Engine ↓ Agent reasoning loop ↓ Tool execution ↓ Memory updates


Task Execution Model

Each task runs an agent reasoning loop.

Lifecycle:

task start ↓ build task context ↓ agent reasoning loop ↓ success detection ↓ store exposed variables ↓ task end


Task Context

Before executing a task, the runtime builds a context object.

Example:

{ "task_id": "find_server", "goal": "Find where the HTTP server is initialized.", "success": "Identify the file and function responsible for startup.", "expose": ["server_file","server_function"], "variables": {} }

Hints such as @no-history and @no-kg are included in this context.


Task Outcomes

A task may terminate with one of several outcomes.

Status Meaning
success goal achieved
failed task attempted but not successful
blocked task cannot proceed due to missing dependency
aborted execution intentionally stopped

Example success result:

{ "status": "success", "summary": "Server initialized in cmd/server/main.go", "exposed": { "server_file": "cmd/server/main.go" } }

Example blocked result:

{ "status": "blocked", "reason": "repository_not_loaded", "message": "Cannot analyze repository because none is loaded." }


Runtime Limits

Safety limits enforced by the runtime.

Limit Purpose
max_steps maximum reasoning steps
max_tool_calls maximum tool executions
timeout maximum task duration

Example configuration:

max_steps = 12 max_tool_calls = 20 timeout = 30s


Goals

Goals enable autonomous agent behavior by scheduling periodic execution of an AWL body.

Syntax:

@goal [max_actions=] Success:
@end Parameters: | Parameter | Required | Description | |---|---|---| | max_actions | no | Max tool calls per cycle (default: 5) | Scheduling is independent from the goal definition. Goals can be run: - From the CLI: `ai-assist /run goal.awl` - Via `event-schedules.json`: `{"prompt": "goals/track_failures.awl", "trigger": {"type": "interval", "every": "30m"}}` - As a one-shot scheduled action The `Success:` field is mandatory. After each cycle, Claude evaluates whether the criterion is met. When met, the goal status is set to "completed" and scheduling stops. ## Variable Persistence Variables exposed by tasks inside the goal body persist between cycles via a JSON sidecar file. This allows the goal to track state across executions. ## Example @start @goal track_failures max_actions=5 Success: Failure rate stays below 10% @task check_status @no-history Goal: Check DCI failure rate for OCP 4.19. Expose: failure_rate, failures @end @if failure_rate > 10 @task create_alert Goal: Write a failure report about ${failures}. @end @end @end @end ## Goal Files Goal AWL scripts live in `~/.ai-assist/goals/`. The monitor mode scans this directory and schedules active goals. State is persisted in `~/.ai-assist/state/goal_.json`. ## Goal Management The agent can create, list, and update goals via built-in tools: - `goal__create` -- generates a `.awl` file - `goal__list` -- scans the goals directory - `goal__update` -- changes goal status (pause, resume, cancel) --- # Summary The Agent Workflow Language enables structured orchestration of AI agents. Key properties: - intent-driven workflows - agent autonomy - task hints controlling context - deterministic execution - extensible architecture