Workflow schema
A V2 workflow is a directed graph stored as JSON or YAML under .agentx/workflows/. The editor's canvas displays the same definition. Use agentx workflow validate <file> before importing a hand-written file.
| Term | Meaning |
|---|---|
| Workflow | The named automation definition |
| Node | One unit of work, such as a trigger, an agent call, or an output action |
| Edge | A connection from one node ID to another; a branch can choose a named output port |
| Run | One execution of a workflow |
| Pause / resume | Stops and later continues a run where supported |
Required structure
| Field | Meaning |
|---|---|
id | Stable lowercase ID using letters, digits, hyphens or underscores |
version | 2 |
title | Human-readable title |
nodes | Nonempty array of { id, type, config } objects |
edges | Connections such as { "from": "start", "to": "report" } |
state | Dispatch control: active, disabled, or quarantined |
status | Review metadata: draft, review, active, or deprecated |
state controls execution. A workflow marked status: "draft" can still run if its state is active. Use state: "disabled" for a proposal that must not fire yet. A quarantined workflow is held because of a detected conflict; investigate that conflict before reactivating it.
A small example
Download demo-report.json. It contains three nodes: trigger.manual, agent, and end. The agent node uses config.agentId: "cx" and a fixed report prompt. Replace cx with your registered agent before using it outside the demo.
agentx workflow validate demo-report.json
agentx workflow add demo-report.jsonThe example is deliberately disabled. After reviewing it, set state to active, import the updated file, and test it:
agentx workflow run demo-report --watch
agentx workflow runs demo-reportThe daemon must have workflows.enabled: true. These commands use http://127.0.0.1:18800 by default; pass --daemon <url> when working against another endpoint where that option is available.
Inputs and error handling
Node configuration can reference earlier outputs with templates such as {{trigger.payload.text}}. A manual run can supply JSON using --input '{"text":"Prepare a summary"}'. Each node may declare retry: { maxAttempts, backoffMs }; retrying an external write can repeat its side effect, so use it only when the action can safely be repeated.
An agent node needs a registered agentId. It can also set timeoutMinutes. When set, AgentX stops the step once that many minutes have passed and frees the agent's slot, and the step fails with "timed out after …s". Leave it empty for no limit. Give agent work room: a review or a code change can take 20 minutes or more. See time limits and cancel. An action.send node needs a live channel and destination. branch uses named ports to choose an edge; checkpoint pauses for review. Node configuration is validated by the corresponding handler, so passing the top-level file validator alone does not prove that credentials or destinations work.
Node types
| Type | What it does |
|---|---|
trigger.manual, trigger.channel, trigger.cron, trigger.hook, trigger.form | Starts a run: by hand, from a channel message, on a schedule, on an on:* event, or from a form |
agent | Runs an agent with a prompt and passes its answer on |
classify | Picks one label with a confidence; the port named after the label fires, or unsure |
transform | Picks or reshapes values from earlier steps |
branch | Chooses an outgoing port; edges match it with fromPort |
rule | Checks a declared rule and can stop the run early |
gateway.parallel | Waits for parallel branches to join |
action.send, action.react, action.editMessage | Sends a message, reacts to one, or edits one, through a channel |
action.createIssue, action.setLabel, action.readLabel, action.logTime | Creates an issue (GitLab or GitHub), or changes labels, reads labels or logs time on a GitLab issue or merge request |
action.callHTTP | Makes an outgoing HTTP request |
action.run, action.builtin | Runs a registered action, or a built-in one, by name |
signal.emit, signal.wait | Publishes a signal, or pauses until one arrives |
timer.boundary | Pauses until a timer runs out |
subProcess | Starts another workflow and waits for it to finish |
checkpoint | Pauses for review until a resume event arrives |
end | Closes the run with the given status |
Each type's config is checked by its handler in src/workflows/nodes/.
Event trigger filters
A trigger.hook node subscribes to an on:* event, such as on:gitlab-mr or on:github-pr. Its config.filter narrows which events start a run. Events that don't match are dropped before any agent is woken.
| Filter | Events | Fires only when |
|---|---|---|
topic | on:n8n | The topic in /webhook/n8n/<topic> is listed |
action | GitLab issue and MR events | The action, such as open, is listed |
mentions | on:gitlab-note | The comment @-mentions a listed username |
noteableType | on:gitlab-note | The comment is on a listed type: merge_request or issue |
assigneesAdded, reviewersAdded, labelsAdded | GitLab issue and MR events | The update added a listed assignee, reviewer, or label |
skipSelfAuthored | Events with an author | The author is not one of this workflow's own agents (off by default) |
ignoreAuthors | Events with an author | The author is not in the list. Leading @ and letter case are ignored |
maxFiresPerTarget | Issue, MR, PR, and note events | This workflow has fired fewer than count times for the same issue, MR, or PR within windowMinutes (default 60) |
Loop guard. With skipSelfAuthored: true, a workflow skips events written by the bot identity of any agent it runs, so a routine's own comment or label change cannot restart it. It is off by default, because label-driven lifecycle workflows react to their own agent's transitions on purpose. To find that identity, AgentX checks the GitLab adapter's username-to-agent map, the signature on AgentX comments, and each agent's gitlabUsernames or githubUsernames in agentMappings. If the identity can't be resolved, the daemon logs this once and only ignoreAuthors applies. On GitLab issue and MR events the identity comes only from the username map. When all agents share one GitLab token, they post as one user, which maps to a single agent, so a workflow run by a different agent won't see those events as self-authored. Comments avoid this because AgentX reads the signature on each comment.
This check can't catch two routines that trigger each other, such as a generator and a critic. Neither one sees its own identity. Use maxFiresPerTarget for that case:
{ "event": "on:gitlab-note",
"filter": { "noteableType": ["merge_request"], "ignoreAuthors": ["ci-bot"],
"maxFiresPerTarget": { "count": 3, "windowMinutes": 60 } } }A note and an update on the same MR count toward the same limit. Every loop-guard skip still claims the event, unless the trigger sets passthrough. That way the adapter's fallback (the project's default agent, or the legacy @-mention path) doesn't wake the agent instead. Every skip is logged as [workflows] <id> skipping <event> (<reason>). The counters are held in memory, so a daemon restart resets them.
The editor's assistant can propose a workflow from a request. Apply to canvas replaces the current graph. Review the agent, input, destination, and error path before saving. The complete implementation is in src/workflows/types.ts and src/workflows/nodes/.
Check it worked
- Terminal: run
agentx workflow validate <file>. It reports no errors. - Terminal: run
agentx workflow list. Your workflow is listed with its state. - Terminal: after a test run,
agentx workflow runs <id>shows the run and its result.
If something is wrong
- The workflow never fires: check
stateisactive(not onlystatus) and that the daemon hasworkflows.enabled: true. - An
agentstep fails: itsagentIdmust match an agent inagentx.json. - An event trigger doesn't fire: look for
[workflows] <id> skippinginagentx daemon logs; a filter or loop guard dropped the event. - A run stops partway:
agentx workflow trace <runId>shows which step failed and why.
