n8n + AxonFlow Integration
AxonFlow gives n8n workflows a way to call the AxonFlow API for policy checks, decision recording, and human-in-the-loop approvals. There are two ways to wire it up.
| Path | When to use |
|---|---|
| Tier 1 — Stock HTTP-node recipe | Works on any n8n install, including n8n Cloud. Four HTTP nodes per governed action. Useful for prototypes or as a fallback when community nodes are not available. |
Tier 2 — Community node (n8n-nodes-axonflow) | Self-hosted n8n. One node, four operations, idempotency by default, paired with the built-in Wait node for HITL resume. |
Tier 3 — verified node on n8n Cloud. Submission to n8n's verified registry is tracked separately and is not gated by this page.
Tier 1 — Stock HTTP-node recipe (any n8n install)
This recipe uses only built-in n8n nodes. It is the fastest path to "n8n is calling AxonFlow" — useful for prototyping or n8n Cloud where community nodes are not currently allowed.
Setup once
In Credentials → New → Header Auth create an AxonFlow Basic credential:
| Field | Value |
|---|---|
| Name | Authorization |
| Value | Basic <base64(clientId:userToken)> |
Why Header Auth and not Bearer Auth? n8n's built-in Bearer Auth class silently drops the header on some versions (n8n#15261). Header Auth attaches the
Authorizationline explicitly and is unaffected.
Wire four HTTP nodes per governed action
[HTTP] Trigger → [HTTP] AxonFlow Check Policy → [IF] allowed?
├── true → [HTTP] downstream action → [HTTP] AxonFlow Record Decision
└── false → [Wait] for webhook → [HTTP] downstream action → [HTTP] AxonFlow Record Decision
(error branch) → [HTTP] AxonFlow Audit Log
| Node | Method + URL | Body (JSON) |
|---|---|---|
| Check Policy | POST {endpoint}/api/v1/mcp/check-input | { "client_id": "{{ $env.AXONFLOW_CLIENT_ID }}", "user_token": "{{ $env.AXONFLOW_USER_TOKEN }}", "tenant_id": "{{ $env.AXONFLOW_CLIENT_ID }}", "connector_type": "n8n", "statement": "{{ $json.action_description }}", "operation": "execute" } |
| Record Decision | POST {endpoint}/api/v1/audit/tool-call | { "tool_name": "{{ $json.tool_name }}", "tool_type": "n8n_decision", "workflow_id": "{{ $workflow.id }}", "input": {{ JSON.stringify($('Trigger').item.json) }}, "output": {{ JSON.stringify($json) }}, "success": true } |
| Audit Log (error branch) | same URL as Record Decision | same body shape, "success": false, "error_message": "{{ $json.error }}" |
| Wait for Approval | POST {endpoint}/api/v1/hitl/queue | { "client_id": "{{ $env.AXONFLOW_CLIENT_ID }}", "original_query": "{{ $json.description }}", "request_type": "workflow_step", "triggered_policy_id": "n8n-manual", "triggered_policy_name": "n8n manual approval", "trigger_reason": "Workflow gate", "severity": "medium", "expires_in_seconds": 86400 } — followed by a built-in Wait node configured for "On Webhook Call" mode. To actually resume on approval, store the Wait node's webhook URL and either trigger it from a polling sidecar (see HITL pattern) or have a reviewer POST to it manually after deciding in the portal. AxonFlow does not POST automatically yet (tracked at #2419). |
Always set an Idempotency-Key header on every HTTP node so n8n's Retry on Fail does not double-record. Template: {{ $execution.id }}-{{ $itemIndex }}-{{ $node.name }}.
Tier 2 — n8n-nodes-axonflow community node
A single community node with four operations, idempotency-key handling, and a credential type that uses Header Auth (not the buggy Bearer Auth class).
Install
n8n GUI (self-hosted)
Settings → Community Nodes → Install → n8n-nodes-axonflow. Restart n8n.
Manual
cd ~/.n8n/custom
npm install n8n-nodes-axonflow
# restart n8n
n8n Cloud does not currently allow unverified community nodes. Use the Tier 1 recipe until this package is verified.
Configure the credential
Credentials → New → AxonFlow API.
| Field | Description |
|---|---|
| Endpoint | Base URL of your AxonFlow Agent. SaaS: https://try.getaxonflow.com. Self-hosted: typically port 8080. |
| Client ID | Your tenant identifier. |
| User Token | Sent as the password half of HTTP Basic auth. Stored encrypted in n8n. |
The four operations
| Operation | Endpoint | Use it… |
|---|---|---|
| Check Policy | POST /api/v1/mcp/check-input | Before a sensitive action — branch on {allowed, block_reason?}. |
| Record Decision | POST /api/v1/audit/tool-call | After a successful action — capture inputs, outputs, success. |
| Audit Log | POST /api/v1/audit/tool-call | From error branches — same endpoint with success: false. |
| Wait for Approval | POST /api/v1/hitl/queue + Wait node | When you need a human signoff — pause until reviewer responds. |
HITL pattern: Wait node + webhook resume
The Wait for Approval operation creates the AxonFlow approval entry and immediately returns the approval_id. To actually pause-and-resume on the reviewer decision, pair it with a built-in Wait node:
AxonFlow Wait for Approval → Wait (mode: On Webhook Call) → downstream action
Configure the Wait node:
- Resume:
On Webhook Call - Webhook Method:
POST - Webhook URL is auto-generated by n8n. Store this URL — you'll either trigger it from a polling sidecar or hand it to a reviewer for manual resume.
Important — AxonFlow does NOT automatically POST to the Wait node yet. That outbound-webhook feature is tracked at #2419 and will land in a future patch. Two paths work today.
Resume path A — polling sidecar (recommended for unattended workflows)
Run a small process that polls AxonFlow for the approval status and POSTs to the Wait node when the reviewer decides. Minimal reference (any language; bash + curl shown):
#!/usr/bin/env bash
# resume-sidecar.sh APPROVAL_ID WEBHOOK_URL
set -euo pipefail
APPROVAL_ID="$1"; WEBHOOK_URL="$2"
POLL_INTERVAL="${POLL_INTERVAL:-10}"
# Cap the total wait at the AxonFlow approval default (24h) plus a small buffer
# so a never-decided approval can't leak the sidecar process forever.
MAX_POLLS="${MAX_POLLS:-9000}" # 9000 × 10s = 25h
for i in $(seq 1 "$MAX_POLLS"); do
RESP="$(curl -sS --fail-with-body \
-u "$AXONFLOW_CLIENT_ID:$AXONFLOW_USER_TOKEN" \
"$AXONFLOW_ENDPOINT/api/v1/hitl/queue/$APPROVAL_ID")" || {
echo "warn: HITL fetch failed (iter=$i) — will retry" >&2
sleep "$POLL_INTERVAL"; continue
}
STATUS="$(echo "$RESP" | jq -r '.data.status')"
case "$STATUS" in
approved|rejected|overridden|expired|cancelled)
curl -sS --fail-with-body -X POST -H 'Content-Type: application/json' \
-d "$(echo "$RESP" | jq '.data')" "$WEBHOOK_URL"
exit 0
;;
esac
sleep "$POLL_INTERVAL"
done
echo "error: HITL approval $APPROVAL_ID timed out after $MAX_POLLS polls" >&2
exit 1
Invoke once from the n8n workflow itself (an Execute Command or HTTP node), or run it as a long-lived process keyed by approval_id. The Wait node receives the reviewer decision payload as $json in the next node.
The terminal-states case (approved | rejected | overridden | expired | cancelled) covers every status the AxonFlow HITL service emits — anything else is a transient pending and keeps polling. If the n8n Wait node has its own wait-timeout and you'd rather not leak the sidecar, kill the process when the workflow expires.
Resume path B — manual portal resume
The reviewer copies the Wait node's webhook URL from the n8n workflow editor and POSTs to it from the AxonFlow portal's approval-detail screen after approving or rejecting. Works without a sidecar; trades automation for simplicity. Recommended for small operations teams or during integration smoke-tests.
Once #2419 ships, AxonFlow will POST to the registered webhook automatically and the polling sidecar can be retired. The Wait-node side of the workflow is unchanged.
Idempotency by default
Every operation sends an Idempotency-Key header. The default template is:
{{ $execution.id }}-{{ $itemIndex }}-{{ $node.name }}
This pins the request to a unique tuple — execution × item × node — so a future server-side deduplicator can reject re-sends from n8n's Retry on Fail. The header is structurally correct today.
Note — platform-side consumption is in flight. Server-side handling of
Idempotency-Keyon/api/v1/mcp/check-input,/api/v1/audit/tool-call, and/api/v1/hitl/queueis tracked at #2420 and lands in v8.0.2. Until then, the sent header is preserved through audit logs but does not yet deduplicate on its own. Build retry-safety into your workflow (e.g. set the Wait node to fail rather than retry on429, or have a downstream IF node skip on a duplicatedecision_id) until the platform side lands.
Override the key only if you have a domain-specific deduplication identifier (e.g. an upstream request ID that you want to dedupe across executions).
Bearer Auth gotcha
n8n's built-in Bearer Auth class silently drops the Authorization header in some versions (n8n#15261). Credentials look configured, the test passes, every actual call returns 401.
The AxonFlowApi credential bundled with this package uses the Header Auth pattern instead — Authorization is built inline from clientId + userToken and attached to every request. You do not need to worry about the bug; it is documented here so you do not get caught by it when building a Tier 1 recipe.
Example workflow
The n8n-nodes-axonflow package ships an importable workflow at examples/governed-loan-workflow.json that wires up:
- HTTP trigger receives a loan request,
- Check Policy evaluates the proposed action,
- IF branch — if
allowed=true, proceeds to issue the loan; ifallowed=false, routes through Wait for Approval and aWaitnode, - After the loan-issuance HTTP node succeeds, Record Decision captures the outcome,
- Error branch routes to Audit Log with
success: false.
Import it via Workflows → Import from File and point the Issue Loan (HTTP) node at your downstream service.
Reference
- Source + readme:
examples/integrations/n8n-axonflow-node/ - n8n Wait node ("On Webhook Call"): docs.n8n.io
- n8n verification guidelines: docs.n8n.io
- n8n Bearer Auth header-drop bug: n8n#15261
- AxonFlow agent API:
/api/v1/mcp/check-input,/api/v1/audit/tool-call,/api/v1/hitl/queue
