Agent and Trigger Reference
This page shows the file format for agents and triggers. You can copy and edit all examples. To learn what custom agents do, see Build Your Own Agents.
If you use Claude Code, Cursor, Codex, or GitHub Copilot, you can install the Autoheal CLI skills. Your coding agent can then create agents for you.
autoheal skill install
Your coding agent gets access to the complete agent schema. It can create, test, and improve agents. This means you do not need to write YAML manually.
See the CLI guide for installation steps.
How the pieces fit
| Piece | What it is | Where it lives |
|---|---|---|
| Agent | What to do: instructions, model, tools, skills, result shape | agents/<agent-name>/agent.yaml in your project's Git repository |
| Trigger | When to run it: an event, filters on that event, and the prompt to send | The Triggers page, or the /api/v1/triggers API |
| Integrations | The systems the agent can reach, such as GitHub, Datadog or PagerDuty | Integrations |
| Agent profile | Where the agent runs: sandbox resources, network, and which integrations, repositories and skills it can see | Settings → Agent Profiles |
| Skills | Reusable know-how that the agent loads when it needs it | skills/<skill-name>/SKILL.md in the same project |
The agent file and the trigger are separate. One agent can have many triggers. For example, you can have one trigger for critical alerts and one trigger for a specific repository. You do not need to copy the agent.
Your first agent
Create agents/pr-summarizer/agent.yaml:
apiVersion: agents.autoheal.ai/v1
name: PR Summarizer
description: Summarizes a pull request for reviewers.
model: claude-sonnet-5
instructions: |
You summarize GitHub pull requests for busy reviewers.
Read the diff and the linked issue, then reply with:
- what changed, in two or three bullets
- anything risky (migrations, config, auth, public API)
- which files a reviewer should read first
tools:
- integration_type: github
operation_type: read
The folder name is the identifier (agents:pr-summarizer). The name field is just the display name.
Agent file reference
Top-level fields
Every agent has these fields:
| Field | Required | Description |
|---|---|---|
apiVersion | Yes | Always agents.autoheal.ai/v1. |
name | Yes | Display name, for example Alert Triage. |
icon | No | A Phosphor icon name, for example siren or shield-check. |
timeout | No | Wall-clock limit for the agent. Default: 10m. Maximum: 1h. See Timeout and limits. |
inputs | No | Named inputs the agent accepts. See Inputs. |
outputs | No | Values extracted from step results. See Steps. |
limits | No | Budget limits for LLM steps: cost, tokens, model requests. See Timeout and limits. |
steps | No | A graph (map) or sequence (array) of steps to execute. See Steps. |
If you use an unknown field, validation will fail. For example, if you write instruction: instead of instructions:, you will get an error.
Steps
When an agent has a steps field, it runs multiple operations. Each step can be:
- An LLM call
- A tool call
- Code execution
- A call to another agent
- Control flow (if/else, while)
Structure
You can organize steps in two ways:
- As a graph (map of steps)
- As a sequence (array of steps)
Graph (map of steps)
Each step has an ID. Use after: to control the order:
steps:
fetch_data:
tool: builtin.tool.datadog-query-logs@1
with: {query: "error", time_range: "1h"}
analyze:
instructions: Find the root cause of these errors
model: claude-sonnet-5
after: [fetch_data]
with:
logs: ${{ steps.fetch_data.output }}
notify:
tool: builtin.tool.slack-post-message@1
after: [analyze]
with:
channel: "#incidents"
text: ${{ steps.analyze.output.summary }}
Steps with no dependencies run at the same time.
Sequence (array of steps)
The array order is the execution order:
steps:
- id: prepare
runtime: bash
code: echo "Starting..." > "$AUTOHEAL_OUTPUT"
- id: process
instructions: Analyze this data
model: claude-sonnet-5
with:
data: ${{ steps.prepare.output }}
- id: finish
runtime: bash
code: echo "Done" > "$AUTOHEAL_OUTPUT"
You can nest a parallel group inside a sequence:
steps:
- id: start
runtime: bash
code: echo "Begin"
- steps: # parallel group
left:
runtime: bash
code: echo "LEFT"
right:
runtime: bash
code: echo "RIGHT"
- id: finish
runtime: bash
code: echo "${{ steps.left.output }} and ${{ steps.right.output }}"
LLM steps
An LLM step calls a model. The step must have instructions. The step can have model:
steps:
analyze:
instructions: Find the root cause of these errors
model: claude-sonnet-5
tools:
- operation_type: read
with:
logs: ${{ steps.fetch_data.output }}
returns:
conclusion: {type: string, required: true}
confidence: number
An LLM step can have these fields: instructions, model, tools, skills, subagents, artifacts, returns.
Calling agents and tools
Call another agent:
steps:
summarize:
agent: agents:incident-summarizer
with:
incident_data: ${{ steps.investigate.output }}
Call a catalog tool:
steps:
deploy:
tool: builtin.tool.kubernetes-apply-manifest@1
with:
manifest: ${{ steps.build.output.manifest }}
Conditionals (if/else)
steps:
check:
instructions: Check severity of this alert
model: claude-sonnet-5
with:
alert: ${{ inputs.alert }}
returns: {severity: string}
high_severity:
if: ${{ steps.check.output.severity == "high" }}
then:
page:
tool: builtin.tool.pagerduty-create-incident@1
with:
title: "High severity alert"
else:
notify:
tool: builtin.tool.slack-post-message@1
with:
channel: "#alerts"
text: "Low severity alert"
Loops (while)
Repeat steps until a condition becomes false. max_iterations is required:
steps:
retry_deploy:
while: ${{ steps.check_status.output.status != "ready" }}
max_iterations: 10
steps:
check_status:
tool: builtin.tool.kubernetes-get-deployment
with:
deployment: my-service
wait:
runtime: bash
code: sleep 5
Code execution
You can run code in a sandbox. The code reads input from AUTOHEAL_INPUT (JSON). The code writes output to AUTOHEAL_OUTPUT (JSON):
steps:
count:
runtime: python
with:
items: ${{ steps.previous.output.list }}
code: |
import json, os
with open(os.environ["AUTOHEAL_INPUT"]) as f:
data = json.load(f)
count = len(data["items"])
with open(os.environ["AUTOHEAL_OUTPUT"], "w") as out:
json.dump({"count": count}, out)
returns: {count: integer}
Supported runtimes: python, bash, node.
Conditional execution (when)
You can control when a step runs. The step can run based on success or failure of another step:
steps:
deploy:
tool: builtin.tool.deploy-service
notify_success:
when: success(deploy)
tool: builtin.tool.slack-post-message
with:
text: "Deploy succeeded"
notify_failure:
when: failure(deploy)
tool: builtin.tool.slack-post-message
with:
text: "Deploy failed"
cleanup:
when: always()
runtime: bash
code: echo "Always runs"
Functions: always(), success(step_id), failure(step_id).
Retries
You can retry a step when it fails:
steps:
api_call:
tool: builtin.tool.http-request
retry:
max_attempts: 3
backoff: exponential
Step outputs
You can extract values from step results. These values become agent outputs:
outputs:
root_cause: ${{ steps.analyze.output.conclusion }}
severity: ${{ steps.check.output.severity }}
Common step fields
| Field | Meaning |
|---|---|
id | Step identifier (required in sequences) |
after | List of step IDs this step waits for (graph only) |
with | Arguments passed to the agent/tool, or variables available to code |
limits | Override the agent's limits for this step |
when | Condition for running: always(), success(step_id), failure(step_id) |
retry | {max_attempts: 3, backoff: exponential} |
Shorthand for single-step agents
Some agents make only one LLM call. You can write the LLM step at the top level. You do not need to use steps. These two agents do the same thing:
# Shorthand (top-level LLM fields)
apiVersion: agents.autoheal.ai/v1
name: Summarizer
instructions: Summarize this incident
model: claude-sonnet-5
# Explicit (steps form)
apiVersion: agents.autoheal.ai/v1
name: Summarizer
steps:
main:
instructions: Summarize this incident
model: claude-sonnet-5
You can use these fields at the top level:
| Field | Required | Description |
|---|---|---|
instructions | Yes | The system prompt. You can write it inline. You can also give the path to a .md file in the agent's folder. |
description | No | What the agent does. When another agent calls this agent as a subagent, it sees this description. |
model | No | The model to use. If you do not set this, the agent uses your deployment's default model. |
tools | No | Which integration tools the agent can call. If you do not set this, the agent can call every integration tool. See Tools and integrations. |
skills | No | Skills the agent can load. See Skills. |
subagents | No | Other agents this agent can call. See Subagents. |
artifacts | No | Artifact templates the agent can create. Each template gives the agent a create_artifact_<template> tool. |
returns | No | A structured result shape. If you do not set this, the agent returns a text answer. See Structured results. |
Instructions
Write short instructions inline:
instructions: |
You triage new alert groups. Decide whether the group is a real
incident or noise, and say who should look at it.
For longer instructions, put the prompt in a Markdown file next to agent.yaml:
instructions: ./instructions.md
agents/
alert-triage/
agent.yaml
instructions.md
The path must start with ./. The file must be inside the agent's folder. You can use declared inputs in instructions. For example: ${{ inputs.service }}.
Model
A model name on its own:
model: claude-sonnet-5
Or a model with options:
model:
name: claude-sonnet-5
max_tokens: 8192 # max output tokens per response
thinking: medium # true | false | minimal | low | medium | high | xhigh
Use a model from your organization's approved list. Common models:
claude-haiku-4-5: fast and cheapclaude-sonnet-5: most agentsclaude-opus-5: hard reasoning tasks
The automatic prompt input
A single-LLM agent always has a prompt input. You do not need to declare it. When you run the agent, the prompt becomes the model's first message. Each other input you send becomes one name: value line.
For example, an agent with these inputs:
inputs:
service:
type: string
required: true
lookback_minutes:
type: integer
default: 60
When run with:
prompt: "Investigate high latency on checkout-api"
service: "checkout-api"
lookback_minutes: 30
The model receives:
Investigate high latency on checkout-api
service: checkout-api
lookback_minutes: 30
You can declare prompt to add a description or mark it optional:
inputs:
prompt:
type: string
description: The question to investigate
dry_run: {type: boolean, default: false}
The input already has type: string, required: true. You can only refine it.
Optional inputs with no value do not get a line. If the caller does not send lookback_minutes and it has no default, the model sees no lookback_minutes: line. The model does not see lookback_minutes: null.
Expressions
Use ${{ }} to read values. You can read inputs, step outputs, and event data. The agent replaces the expression with the value when it runs.
Triggers use the same expression language. A trigger can read event data with ${{ event.data.field }}. An agent can read inputs with ${{ inputs.field }} and step outputs with ${{ steps.step_id.output }}.
Read step outputs
You can get the result from a step that ran before:
steps:
fetch:
tool: builtin.tool.datadog-query-logs@1
with:
query: "error"
analyze:
instructions: Find the cause of these errors
with:
logs: ${{ steps.fetch.output }}
steps.<step-id>.output gives you the full result from that step. For structured results, use dot notation:
steps:
check:
instructions: Check the severity
returns:
level: string
count: integer
notify:
tool: builtin.tool.slack-post-message@1
with:
text: "Found ${{ steps.check.output.count }} errors at level ${{ steps.check.output.level }}"
Read inputs
You can get a value that was passed to the agent:
inputs:
service: string
time_range: string
steps:
fetch:
tool: builtin.tool.datadog-query-metrics@1
with:
service: ${{ inputs.service }}
time_range: ${{ inputs.time_range }}
Compare values
Use ==, !=, >, <, >=, <= to compare:
steps:
check:
instructions: Count the errors
returns: {count: integer}
alert:
if: ${{ steps.check.output.count > 10 }}
then:
page:
tool: builtin.tool.pagerduty-create-incident@1
Comparison operators:
| Operator | Meaning | Example |
|---|---|---|
== | Equal to | ${{ steps.check.output.status == "ready" }} |
!= | Not equal to | ${{ steps.check.output.status != "ready" }} |
> | Greater than | ${{ steps.count.output > 100 }} |
< | Less than | ${{ steps.count.output < 10 }} |
>= | Greater than or equal | ${{ steps.severity.output >= 3 }} |
<= | Less than or equal | ${{ steps.severity.output <= 1 }} |
Combine conditions
You can combine conditions. Use && (and) and || (or):
# Both conditions must be true
if: ${{ steps.check.output.status == "failed" && steps.check.output.retry_count < 3 }}
# At least one condition must be true
if: ${{ steps.check.output.severity == "critical" || steps.check.output.error_count > 100 }}
Pass whole objects
You can reference an object without selecting a field. The whole object passes through:
steps:
get_pr:
tool: builtin.tool.github-get-pull-request@1
with:
pr_number: ${{ inputs.pr_number }}
analyze:
instructions: Review this pull request
with:
pull_request: ${{ steps.get_pr.output }} # passes the whole PR object
Use in text
You can put expressions in text. This builds messages:
with:
prompt: |
Investigate high latency on ${{ inputs.service }}.
Error count: ${{ steps.fetch.output.count }}
Time range: ${{ inputs.time_range }}
Inputs
You can declare inputs. A trigger or caller can pass structured values. The trigger or caller does not need to pass only a single prompt:
inputs:
service:
type: string
description: The service the alert fired on
required: true
lookback_minutes:
type: integer
default: 60
dry_run:
type: boolean
default: false
Supported types: string, number, integer, boolean, object, array. An input is optional. To make it required, set required: true.
Structured results
By default, an agent answers in text. Add returns when something downstream needs fields it can read.
You can use returns in two ways:
- A single type:
returns: string[]orreturns: number - A field map:
returns: {verdict: string, confidence: number}
Single-value returns
When an agent returns a simple value, use the type directly:
returns: string[] # an array of strings
returns: number # a single number
Supported types: string, number, integer, boolean, object. Add [] for arrays.
Field map
When the result has multiple fields:
returns:
verdict:
type: string
enum: [incident, noise, needs_human]
required: true
confidence: {type: number, minimum: 0, maximum: 1, required: true}
summary: string
affected_services: string[] # [] makes it an array
Each field can be a type shorthand or a JSON Schema fragment:
- Shorthand:
string,number,integer,boolean,object. Add[]for an array (string[]). Shorthand fields are optional by default. - Explicit (fragment): A JSON Schema object. You can set
enum,minimum,description, and more. Addrequired: trueto make the field required. Without that flag, the field is optional.
Field names must be snake_case.
Nested objects and arrays
Nested fields follow the same rule. Each field sets its own required: true. Do not use JSON Schema's required: [a, b] list. Validation rejects it.
returns:
owner:
type: object
required: true
properties:
team: {type: string, required: true}
oncall: string # optional
risks:
type: array
items:
type: object
properties:
risk: {type: string, required: true}
evidence: string # optional
For an array, mark the array field itself (risks above). Do not mark the items. Validation rejects an items field with a required flag.
Timeout and limits
Timeout
timeout sets the time limit for the agent or a step. The run stops when it reaches the timeout.
apiVersion: agents.autoheal.ai/v1
name: Alert Triage
timeout: 10m # agent timeout, at most 1h
instructions: ...
For multi-step agents, you can give each step its own timeout:
timeout: 15m # whole agent timeout
steps:
probe:
runtime: python
code: ...
timeout: 30s # this step must finish in 30s
triage:
instructions: Analyze the results
timeout: 5m # this step must finish in 5m
A step without a timeout uses the agent's timeout. A step's timeout cannot be longer than the agent's timeout.
Default: 10m. Maximum: 1h.
Limits (LLM steps only)
The limits block sets cost and request limits. You can use it only for LLM steps. When you set limits, the model sees what it has used before each request.
# Single-LLM agent
apiVersion: agents.autoheal.ai/v1
name: Triage
timeout: 10m
limits:
cost_usd: 2 # advisory: stop before spending $2
tokens: 200000 # advisory: stop before using 200k tokens
model_requests: 50 # hard stop: fail at the 51st request
instructions: ...
# LLM step in a multi-step agent
steps:
triage:
instructions: Analyze and triage
timeout: 5m
limits:
cost_usd: 1
model_requests: 30
| Field | Type | Meaning |
|---|---|---|
cost_usd | Advisory | Target spend in USD at list price. This includes subagents. The model sees what it has used. The limit does not stop the run. |
tokens | Advisory | Target tokens (input minus cache reads, plus output). This includes subagents. The model sees what it has used. The limit does not stop the run. |
model_requests | Hard stop | Number of model requests the step can make. The run fails when the next request would exceed this limit. Range: 1–1000. Default: 300. |
Time target: The model sees a time target. The platform computes it as timeout − buffer. The buffer is 10% of the timeout (at least 30s). This gives the model room to finish before the hard timeout. For a 10m timeout, the time target is 9m.
Tools and integrations
tools decides which integration tools the agent can call. Each entry is a rule. A tool is allowed if it matches any rule. Within one rule, every key must match.
| Key | Values |
|---|---|
integration_type | The integration, for example github, datadog, pagerduty, slack |
operation_type | read or write |
name | One specific tool, by name |
Common patterns:
# Read anything, write nothing
tools:
- operation_type: read
# Read Datadog and GitHub, and post to Slack
tools:
- {integration_type: datadog, operation_type: read}
- {integration_type: github, operation_type: read}
- {integration_type: slack, operation_type: write}
# No integration tools at all
tools: []
Rules for what is always available:
- If you do not set
tools, the agent gets every integration tool. This includes reads and writes. Use an explicit list for agents that run on their own. - Platform tools (catalog, incidents, memories, instructions, and other Autoheal built-in tools) are always available. You do not need to grant them.
- The shell (
bash) is always available inside the agent's sandbox.
A rule grants a type of integration (like github). It does not grant a specific connection. The agent profile sets which connection the agent uses.
Governance policies can require a person's approval before matching calls run, for example every write to PagerDuty. A policy applies to every agent, whatever its tools list says.
Skills
A skill is a Markdown playbook. The agent loads it only when the agent decides it is relevant. This keeps long runbooks out of the prompt until the agent needs them. See Skills.
skills:
- skills:co-firing-correlation # loads skills/co-firing-correlation/SKILL.md
- ./skills:local-runbook # a skills/ folder inside this agent's folder
Subagents
An agent can call other agents as tools. Each key becomes a tool. The tool is named invoke_<key>.
Referencing existing agents
subagents:
investigate_hypothesis:
agent: ./agents:hypothesis-investigator
description: Investigates one candidate cause and returns evidence
summarizer:
agent: agents:incident-summarizer
description: Summarizes findings into a brief report
Agent references come in three forms:
| Reference | Points to |
|---|---|
agents:incident-summarizer | agents/incident-summarizer/ at the project root |
./agents:hypothesis-investigator | A private child inside this agent's folder: agents/<this-agent>/agents/hypothesis-investigator/ |
../agents:verifier | A sibling of this agent |
Add a description when the model needs it. The description helps the model decide when to call the subagent. Keys must be snake_case.
Inline subagents
You can also define a subagent directly in the parent's agent.yaml:
subagents:
formatter:
description: Formats the incident summary for posting to Slack
instructions: |
Format the incident summary as a Slack message with:
- Severity emoji (🔴 sev1, 🟡 sev2, 🟢 sev3)
- Root cause in bold
- Proposed mitigation as a bulleted list
model: claude-haiku-4-5
returns: string
Inline subagents must have description and instructions. They can have model, tools, skills, artifacts, returns. They cannot have their own subagents (no nesting).
Inline subagents resolve instruction files and skills relative to the parent's agent.yaml. They run at the parent's Git version.
How references work
You have now seen agents, skills, and subagents. Here is how they reference each other:
your-project/
├── agents/
│ ├── pr-summarizer/
│ │ └── agent.yaml
│ ├── incident-triage/
│ │ └── agent.yaml
│ └── deployment-analyzer/
│ └── agent.yaml
└── skills/
├── kubernetes-debug/
│ └── SKILL.md
└── database-queries/
└── SKILL.md
The folder name becomes the identifier:
agents/pr-summarizer/→agents:pr-summarizerskills/kubernetes-debug/→skills:kubernetes-debug
Where you use them:
- Triggers:
run.agent: agents:pr-summarizer - Steps:
agent: agents:incident-triage - Subagents:
agent: agents:deployment-analyzer - Skills:
skills: [skills:kubernetes-debug]
Rules:
- Use the folder name. Do not use the
namefield in the YAML. - Always use the prefix
agents:orskills:. - Paths are relative to your Git repository root.
Trigger reference
A trigger starts an agent run when an event occurs. Triggers are separate from agents. One agent can have many triggers.
name: Investigate critical alert groups # required
on: # required: one event, plus filters
alertgroup.created:
- status: firing
- severity: critical
run: # required: which agent to start
agent: agents:alert-triage
profile: sre-readonly # optional
branch: main # optional, defaults to the project's default branch
with: # the agent's inputs
prompt: "Triage ${{ event.data.title }}"
limits: # optional
max_con: 2
rate_per_min: 10
Create triggers on the Triggers page or use the Autoheal CLI.
Trigger fields
| Field | Required | What it does |
|---|---|---|
name | yes | Display name for the trigger. Shown in trigger lists and run history. |
on | yes | Which event fires the trigger and what filters must match. Exactly one event type, with a list of filters. See Events and Filters. |
run | yes | Which agent to start, which profile to run in, and which Git branch to load from. See Which agent to run. |
with | yes | The inputs to pass to the agent. Must include prompt. Can reference event data with ${{ event.data.<path> }} expressions. See Passing event data to the agent. |
limits | no | Rate limiting: max_con (max concurrent runs) and rate_per_min (max new runs per minute). Events over limit are dropped, not queued. See Limits. |
Events
on names exactly one event type.
| Event | When it fires |
|---|---|
alertgroup.created | A new alert group is created from your alert sources |
github.<event> | A webhook from a connected GitHub integration, for example github.pull_request, github.push, github.release, github.issues, github.check_run, github.workflow_run, github.security_advisory. Any GitHub webhook event name works. |
The Triggers page lists every available event with their fields.
Useful fields on alertgroup.created:
| Field | Example |
|---|---|
status | firing, resolved |
severity | critical, high, medium, low |
title | High error rate on checkout-api |
alert_count | 4 |
group_labels | {alertname: HighErrorRate, service: checkout-api} |
alert_payload.common | Labels and annotations every alert in the group shares |
alert_payload.members | Each alert in the group |
GitHub events carry the standard GitHub webhook payload, for example action, repository.full_name, pull_request.number and pull_request.base.ref.
Filters
Each event has a list of filters. All filters must match. Paths are read from the event's data. Use dots for nesting.
on:
github.pull_request:
- integration_name: github-engineering # only this GitHub connection
- repository.full_name: acme/payments # equals
- pull_request.base.ref: main
- anyOf: # at least one must match
- action: opened
- action: synchronize
| Filter | Meaning |
|---|---|
path: value | Equals |
path: {equals: value} / {not_equals: value} | Equals / does not equal |
path: {matches: "regex"} / {not_matches: "regex"} | Matches a regular expression (up to 1,024 characters) |
path: {contains: value} | The text or list contains the value |
path: {exists: true} / {exists: false} | The field is present / absent |
path: {greater_than: n}, greater_than_or_equals, less_than, less_than_or_equals | Number comparisons |
anyOf: [filter, filter, ...] | At least one of two or more filters matches. Allowed once per trigger, and cannot be nested. |
integration_name: name | Only events from this integration connection, which must be installed and enabled in the project. Allowed once, and only for integration events such as GitHub. |
path[*].field: value | Matches if any item in a list matches, for example pull_request.labels[*].name: hotfix |
A filter on a field the event does not have counts as not matching. Exception: {exists: false}.
Which agent to run
The run field says which agent to start and where to run it:
run:
agent: agents:alert-triage # required
profile: sre-readonly # optional
branch: main # optional
| Field | Required | What it does |
|---|---|---|
agent | yes | Which agent to run. Use agent references like agents:alert-triage. See How references work. |
profile | no | Which agent profile to run in. Agent profiles control sandbox resources, network access, and which integrations, repositories, and skills the agent can see. Defaults to the project's default profile. Configured in Settings → Agent Profiles. |
branch | no | Which Git branch to load the agent from. Defaults to the project's default branch (usually main). Use this to test changes on a feature branch before merging. |
Passing event data to the agent
with becomes the agent's inputs. Values can include ${{ }} expressions (the same expression language as agents use). Expressions can read:
| Expression | Value |
|---|---|
event.data.<path> | The event payload, for example event.data.pull_request.number |
event.type | For example github.pull_request |
event.id, event.time | The event's ID and timestamp |
event.integration_name | The integration connection that sent the event |
trigger.id, trigger.revision | The trigger that fired |
with:
prompt: |
Review pull request #${{ event.data.pull_request.number }}
in ${{ event.data.repository.full_name }}.
Title: ${{ event.data.pull_request.title }}
pull_request: ${{ event.data.pull_request }} # whole objects pass through as objects
promptis required. Other keys must match the agent's declared inputs.- Missing paths render as empty. This does not cause an error.
- Set the profile in
run.profile. Do not set it inwith.
Limits
You can control how many runs a trigger can start:
| Field | Meaning |
|---|---|
max_con | Maximum concurrent runs from this trigger |
rate_per_min | Maximum new runs per minute |
Events over a limit are dropped. They are not queued. Dropped events appear in run history as rate_limited.
Trigger run history
Failed trigger runs show the stage they stopped at:
| Stage | What went wrong |
|---|---|
rate_limited | Over max_con or rate_per_min |
input_rendering | A with expression could not be rendered |
target_validation | The agent, branch, or profile no longer exists. Or the inputs do not match the agent. |
dispatch | The agent run could not be started |
You can replay or manually fire triggers for testing.
End-to-end examples
Triage critical alerts and page the owner
agents/alert-triage/agent.yaml:
apiVersion: agents.autoheal.ai/v1
name: Alert Triage
icon: bell-ringing
description: Decides whether a new alert group is a real incident and who owns it.
model:
name: claude-sonnet-5
thinking: medium
instructions: ./instructions.md
tools:
- operation_type: read
- {integration_type: pagerduty, operation_type: write}
skills:
- skills:co-firing-correlation
returns:
verdict:
type: string
enum: [incident, noise, needs_human]
required: true
cause_service: {type: string, required: true}
routed_to: string
summary: {type: string, required: true}
agents/alert-triage/instructions.md:
You triage new alert groups for the on-call engineer.
1. Find which service is the cause and which alerts are symptoms.
2. Check recent deploys and config changes for the cause service.
3. If it is a real incident, page the owning team once in PagerDuty.
4. If it is noise, say which monitor is noisy and why.
Never page more than one team. If you are not sure, return needs_human.
The trigger:
name: Triage critical and high alert groups
on:
alertgroup.created:
- status: firing
- anyOf:
- severity: critical
- severity: high
run:
agent: agents:alert-triage
profile: production-readonly
with:
prompt: >-
${{ event.data.title }} (${{ event.data.severity }},
${{ event.data.alert_count }} alerts). Labels: ${{ event.data.group_labels }}.
Find out what happened and who should look at it.
limits:
max_con: 3
rate_per_min: 10
To page through PagerDuty only after a person approves, add a governance policy on PagerDuty writes. You do not need to change the agent.
Review pull requests to main
agents/code-reviewer/agent.yaml:
apiVersion: agents.autoheal.ai/v1
name: Code Reviewer
icon: git-pull-request
model: claude-sonnet-5
instructions: |
You review pull requests for correctness and operational risk.
Read the diff in the cloned repository. Comment only on real problems:
bugs, missing error handling, unsafe migrations, secrets, risky config.
Post one review comment on the pull request with your findings,
or "No blocking issues" if there are none.
inputs:
prompt: {type: string, required: true}
repository: {type: string, required: true}
pr_number: {type: integer, required: true}
tools:
- {integration_type: github, operation_type: read}
- {integration_type: github, operation_type: write}
The trigger:
name: Review payments pull requests
on:
github.pull_request:
- integration_name: github-engineering
- repository.full_name: acme/payments
- pull_request.base.ref: main
- pull_request.draft: false
- anyOf:
- action: opened
- action: synchronize
run:
agent: agents:code-reviewer
profile: payments-repos
with:
prompt: >-
Review pull request #${{ event.data.pull_request.number }}
in ${{ event.data.repository.full_name }}:
${{ event.data.pull_request.title }}
repository: ${{ event.data.repository.full_name }}
pr_number: ${{ event.data.pull_request.number }}
limits:
max_con: 2
rate_per_min: 20
The payments-repos profile clones acme/payments into the sandbox. The agent can then read the code and the diff.
Write release notes for pre-releases
apiVersion: agents.autoheal.ai/v1
name: Release Notes
icon: megaphone
model: claude-haiku-4-5
instructions: |
Write release notes for the release in the prompt. Group merged pull
requests into Features, Fixes and Internal. Post the notes to the
#releases Slack channel.
tools:
- {integration_type: github, operation_type: read}
- {integration_type: slack, operation_type: write}
name: Release notes for pre-releases
on:
github.release:
- action: published
- release.prerelease: true
run:
agent: agents:release-notes
with:
prompt: >-
Write notes for ${{ event.data.release.tag_name }}
in ${{ event.data.repository.full_name }}.
Escalate critical security advisories
name: Critical security advisories
on:
github.security_advisory:
- security_advisory.severity: critical
run:
agent: agents:vulnerability-triage
with:
prompt: >-
New critical advisory ${{ event.data.security_advisory.ghsa_id }}:
${{ event.data.security_advisory.summary }}.
Find which of our repositories use the affected package.
An agent with subagents
agents/
incident-response/
agent.yaml
instructions.md
agents/
hypothesis-investigator/agent.yaml
adversarial-verifier/agent.yaml
apiVersion: agents.autoheal.ai/v1
name: Incident Response
icon: siren
model:
name: claude-opus-5
thinking: high
instructions: ./instructions.md
tools:
- operation_type: read
subagents:
investigate_hypothesis:
agent: ./agents:hypothesis-investigator
description: Investigates one candidate cause. Call once per hypothesis, in parallel.
adversarial_verifier:
agent: ./agents:adversarial-verifier
description: Argues against the leading explanation and returns a verdict.
returns:
root_cause: {type: string, required: true}
confidence: {type: number, minimum: 0, maximum: 1, required: true}
severity: {type: string, enum: [sev1, sev2, sev3, sev4], required: true}
proposed_mitigation: string
The child agents are ordinary agent.yaml files. Use a cheaper model for them, such as claude-sonnet-5. The expensive model is then used only for the final judgment.