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
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:
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.
Values support ~ expansion and $ENV_VAR substitution. The file is optional.
Precedence (highest wins):
- CLI / caller-provided variables (
ai-assist /run script.awl key=value) - User-defined variables from
variables.yaml - 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 > 0when onlyfailed_jobswas exposed) - Referencing a
collectvariable inside the loop that produces it (thecollectvariable 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:
inspect_awl_script— included asvariable_warningsin the resultexecute_awl_script— blocks execution and returns an error- 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"withvaluesholding the task'sExpose:variables. These are taken as is (methodfinish_tool); only keys missing fromvaluesgo through the usual extraction (JSON block in the reply, nudge, report sections).status="failed"with areasonwhen 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
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
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
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
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
@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_