n8n Implementation Guide
Status
Canonical implementation guide. Related specs: Synedrix OS - Agent Spec, Synedrix OS Workflow Contract Spec, Synedrix OS Outcome Spec, Synedrix OS - Agent Runtime Policy Spec, Synedrix OS Naming Convention Spec, Synedrix OS Orchestration & Runtime Strategy.
Purpose
The Synedrix OS specifications define abstract contracts for agents, workflows, outcomes, and governance. This guide bridges those abstractions to concrete n8n constructs — which node implements which spec concept, how fields map, how execution patterns translate to workflow topologies, and how the outcome envelope is assembled.
Scope boundary (v0.2): As of ADR-OS-001, n8n is the Orchestration Layer (Layer 3). This guide covers n8n orchestration and simple acyclic agents (
sequential,conditional,parallel,fan-out-fan-in,pipeline) that may run as n8n AI Agent nodes. For stateful/cyclic/multi-agent agents, build in LangGraph using the LangGraph Runtime Implementation Guide; n8n still owns the trigger, surrounding sequence, policy/HITL gates, and outcome persistence, and invokes LangGraph as a governed step.
Every n8n workflow that implements a Synedrix OS agentic workflow contract must follow the mappings in this guide. Deviating from these mappings makes the workflow non-compliant: it will not produce well-formed outcomes, correlation IDs will break lineage, and the governance layer cannot reason about execution.
Scope
- Mapping from agent spec sub-objects to n8n node configuration
- Mapping from
execution_patternvalues to n8n workflow topology - Mapping from
step_typeto n8n node types - Outcome envelope assembly in n8n Code nodes
- Correlation ID and causation ID propagation
- Policy gate and HITL approval routing
- Memory profile mapping to n8n memory sub-nodes
- n8n workflow and node naming conventions
Not in scope: n8n deployment configuration, credential management, Hookdeck ingress setup, Langfuse trace integration specifics, Supabase schema details (see Agent DB Schema).
Quick reference: OS concept → n8n construct
How each Synedrix OS concept maps onto a concrete n8n node or field.
| OS concept | n8n construct |
|---|---|
| Agent (agent-driven step) | AI Agent node (@n8n/n8n-nodes-langchain.agent) |
prompt_policy.base_prompt_ref | AI Agent node → System Message field |
runtime_policy.primary_model | Chat Model sub-node (OpenAI, Anthropic, Ollama) |
tool_permissions entry | Tool sub-node connected to AI Agent node |
| MCP tool | MCP Client Tool node (@n8n/n8n-nodes-langchain.mcpClientTool) |
memory_profile.working_memory | Buffer Window Memory sub-node |
memory_profile.canonical_memory_access | Supabase Postgres Chat Memory or Code tool |
| Deterministic step | Native n8n node (Set, Code, Merge, Filter, HTTP Request) |
execution_pattern: sequential | Linear node chain |
execution_pattern: parallel | Parallel branches → Merge node |
execution_pattern: conditional | IF node or Switch node |
execution_pattern: fan-out-fan-in | Parallel branches → Merge → Code (aggregation) |
execution_pattern: iterative | IF node loop back to AI Agent |
execution_pattern: consensus | Parallel AI Agent branches → Code (quorum evaluation) |
execution_pattern: hierarchical | Execute Workflow node |
execution_pattern: pipeline | Linear Set/Code/AI Agent chain |
execution_pattern: reflect-and-revise | AI Agent → Code (quality check) → IF → loop or exit |
| Outcome envelope | Code node after AI Agent node |
correlation_id | $execution.customData.correlationId (set at workflow entry) |
| Policy gate | Execute Workflow (Policy Guard) → IF node on decision |
| HITL approval | Execute Workflow (HITL) → Wait node → webhook callback |
| Workflow outcome | Code node at workflow end, written to workflow_outcomes via Supabase node |
1. Agent spec → n8n AI Agent node
The n8n AI Agent node (@n8n/n8n-nodes-langchain.agent) is the implementation surface for every agent-driven step in a Synedrix OS workflow. It is a composite node — it accepts sub-nodes for the chat model, tools, and memory.
1.1 Identity and naming
The AI Agent node’s display name must follow the Synedrix OS naming convention:
[{LAYER}] {agent_id} · {name}Examples:
[OS] os-agt-002 · Policy Guard
[TRD] trd-agt-001 · Signal Ingestion
[TRD] trd-ctl-risk-manager-001 · Risk Manager VETOThis ensures agents are identifiable in n8n execution logs, trace outputs, and workflow exports. See Naming Convention Spec §5 for the full convention.
1.2 Prompt policy → System Message
| Agent spec field | n8n AI Agent node field |
|---|---|
prompt_policy.base_prompt_ref | System Message (primary content) |
prompt_policy.role_prompt_ref | Appended to System Message as a role block |
prompt_policy.safety_overlay_ref | Appended to System Message as a safety block |
prompt_policy.domain_overlay_ref | Appended to System Message (domain context) |
prompt_policy.tenant_overlay_ref | Injected at runtime via expression in System Message |
prompt_policy.environment_overlay_ref | Injected at runtime via expression |
The System Message field in n8n supports expressions. Use {{ }} syntax to inject runtime values (tenant ID, environment, correlation ID) from the input payload.
You are the Policy Guard Agent (os-agt-002).
...
Correlation ID: {{ $json.correlation_id }}
Domain context: {{ $json.domain }}1.3 Runtime policy → Chat Model sub-node
The AI Agent node requires a Chat Model sub-node that implements runtime_policy.primary_model:
runtime_policy field | n8n Chat Model sub-node |
|---|---|
primary_model: gpt-4o | OpenAI Chat Model node → model: gpt-4o |
primary_model: claude-sonnet-4-6 | Anthropic Chat Model node → model: claude-sonnet-4-6 |
primary_model: ollama/llama3.1:8b | Ollama Chat Model node → model: llama3.1:8b, base URL: http://localhost:11434 |
primary_model: vllm/mistralai/Mistral-7B | HTTP-based Chat Model (custom) pointing at vLLM endpoint |
model_type: hybrid | Two separate Chat Model nodes; select via an IF node upstream |
token_budget.output_max maps to the Max Tokens field on the Chat Model sub-node.
There is no native n8n field for latency_budget_ms. Enforce it via a workflow-level timeout setting and a Wait node with a timeout callback that escalates if the agent has not responded within the budget.
1.4 Tool permissions → Tool sub-nodes
Each entry in tool_permissions maps to a Tool sub-node connected to the AI Agent node. The AI Agent node can have any number of tool sub-nodes attached.
tool_permissions field | Maps to |
|---|---|
tool_id | Tool node display name: tool:{tool_id} |
access_mode: read | Tool node configuration restricted to read operations only |
access_mode: read-write | Tool node with full operation scope |
side_effect_class: write | Tool node must be wrapped with Policy Guard call when approval_tier requires it |
approval_tier: policy | n8n sub-workflow: call Policy Guard Agent before tool execution |
approval_tier: hitl | n8n Wait node: pause execution, call HITL Gateway Agent |
audit_mode: full | Emit agent.tool.authorized event after tool call (Code node) |
Tool sub-node types by tool category:
| Tool category | n8n tool sub-node |
|---|---|
| HTTP API call | @n8n/n8n-nodes-langchain.toolHttpRequest |
| Custom function / script | @n8n/n8n-nodes-langchain.toolCode |
| MCP server tool | @n8n/n8n-nodes-langchain.mcpClientTool |
| SQL / database query | @n8n/n8n-nodes-langchain.toolSqlAgent or custom Code tool |
| Supabase read | Custom Code tool wrapping n8n-nodes-base.supabase |
| Vector search (Qdrant) | Custom Code tool making HTTP request to Qdrant REST API |
MCP tools: The MCP Client Tool node connects to an MCP server declared in the agent’s interop.mcp_profile. The mcp_tool_policy.mcp_server_allowlist from the runtime policy must be validated — if the MCP server ID is not on the allowlist, the tool sub-node must not be connected.
Tool authorization check: Before connecting tool sub-nodes to the AI Agent node, the orchestrating workflow must verify the agent’s active tool set (resolved from tool_authorization_policy). Any tool not in the active tool set must be omitted from the node connections for that invocation.
1.5 Memory profile → Memory sub-nodes
The AI Agent node accepts a Memory sub-node for in-session working memory.
memory_profile field | n8n Memory sub-node |
|---|---|
working_memory: buffer-window | @n8n/n8n-nodes-langchain.memoryBufferWindow — set window size from working_memory_ttl_ms equivalent |
working_memory: postgres | @n8n/n8n-nodes-langchain.memoryPostgresChat — connects to Supabase PG17 |
canonical_memory_access: read | Code tool wrapping a Supabase SELECT |
semantic_memory_access: read | Code tool making HTTP GET to Qdrant /collections/{name}/points/search |
graph_memory_access: read | Code tool making HTTP POST to Neo4j Bolt API |
temporal_memory_access: read | Code tool making HTTP GET to QuestDB REST SQL endpoint |
audit_memory_access: read | Not permitted at runtime — audit reads are Audit & Lineage Agent only |
Only one memory sub-node can be attached to an AI Agent node. For agents that need multiple memory types, implement additional memory retrievals as Tool sub-nodes that the agent explicitly calls during its reasoning turn.
2. Workflow contract → n8n workflow structure
A Synedrix OS workflow contract (spec v0.4) maps to an n8n workflow with a required structural skeleton.
2.1 Required structural sections
Every n8n workflow implementing a Synedrix OS contract must contain these sections in order:
flowchart TD A[TRIGGER]:::term --> B[INPUT VALIDATION — deterministic step, schema-validate transform]:::agent B --> C[CORRELATION ID INIT — Code node: extract or generate correlation_id and causation_id]:::agent C --> D[EXECUTION PATTERN BODY — the pattern-specific node topology, see §3]:::agent D --> E[OUTCOME ASSEMBLY — Code node: wrap result in outcome envelope]:::agent E --> F[OUTCOME PERSIST — Supabase node: INSERT to workflow_outcomes]:::agent F --> G[OUTCOME EVENT EMIT — HTTP Request node: POST os.outcome.* event to event bus]:::term
2.2 Workflow contract fields → n8n workflow settings
| Workflow contract field | n8n equivalent |
|---|---|
workflow_id | Workflow Tags: add wf_id:{workflow_id} tag |
name | Workflow display name (follows naming convention §5 of Naming Spec) |
trigger_type: event | Webhook trigger node or Hookdeck → n8n webhook |
trigger_type: schedule | Schedule trigger node |
trigger_type: human-command | Manual trigger or form trigger |
trigger_type: api-call | Webhook trigger node |
trigger_type: callback | Webhook trigger node (awaiting external callback) |
idempotency_strategy | Generate a stable invocation idempotency key; correlation_id groups work but is not unique |
retry_policy | n8n workflow error workflow + retry configuration |
error_policy.escalation_target | Error workflow: Execute Workflow node to escalation sub-workflow |
version | Workflow description field (record the contract version here) |
2.3 Correlation ID and causation ID
Correlation ID must be set at workflow entry and propagated to every node that reads or writes data.
// Code node: CORRELATION ID INIT (place immediately after trigger)
const incomingCorrelationId = $json.correlation_id || $json.correlationId;
const correlationId = incomingCorrelationId || `corr-${$execution.id}`;
const causationId = $json.causation_id || $json.event_id || `exec-${$execution.id}`;
// Store in execution custom data for downstream access
await $execution.customData.set('correlationId', correlationId);
await $execution.customData.set('causationId', causationId);
await $execution.customData.set('workflowStartedAt', new Date().toISOString());
return [{ json: { ...$json, correlation_id: correlationId, causation_id: causationId } }];Every subsequent node accesses correlation ID via:
const correlationId = $execution.customData.get('correlationId');Or passes it forward via the JSON payload if the expression {{ $json.correlation_id }} is set in each node.
Invariant: A workflow that writes to any Supabase table or emits any OS event without a correlation_id is non-compliant. This is a lineage gap and a compliance breach (Outcome Spec v0.2 § Invariants).
3. Execution pattern → n8n workflow topology
3.1 sequential
Linear chain. Each node’s output connects to the next node’s input. Use depends_on in the contract to set the connection order.
flowchart TD A[Step 1: deterministic or agent-driven]:::agent --> B[Step 2: deterministic or agent-driven]:::agent --> C[Step N: ...]:::agent
Mixed step types in a sequential workflow:
step_type: deterministic→ native n8n node (Set, Code, HTTP Request, etc.)step_type: agent-driven→ AI Agent node
Error path: connect the error output of each AI Agent node to a shared Error Handler sub-workflow that assembles a failure outcome and escalates.
3.2 parallel
All tasks launch simultaneously. Use multiple output branches from a single node (or a Split in Batches node for dynamic fan-out) and collect with a Merge nodeset toWait for all branches.
flowchart TD T[Trigger / upstream step]:::term --> A[AI Agent: task-A]:::agent T --> B[AI Agent: task-B]:::agent T --> C[AI Agent: task-C]:::agent A --> M[Merge: wait for all]:::check B --> M C --> M N[all merge here]:::term -.-> M
wait_for: all → Merge mode: Merge by positionorMerge all data.
wait_for: first-success → Use a Split Out + webhook callback pattern (advanced).
3.3 conditional
Use an IF node(single condition) orSwitch node (multiple branches). The condition_source field names the upstream node or field whose output drives the routing.
flowchart TD
P[Policy Guard AI Agent]:::agent --> S{Switch: on decision field}:::check
S -- allow --> A[Execute Action AI Agent]:::agent
S -- escalate --> H[HITL sub-workflow]:::agent
S -- block --> D[Dead Letter Code node]:::warn
The Switch node routes on {{ $json.result.decision }} (the decision field from the Policy Guard’s outcome.result).
3.4 fan-out-fan-in
Fan out to parallel AI Agent branches. Aggregate with a Merge nodefollowed by aCode node(aggregation logic) or anAI Agent node (aggregator_agent_role).
flowchart TD U[Upstream step]:::term --> A[AI Agent: risk-analyser]:::agent U --> B[AI Agent: compliance-checker]:::agent U --> C[AI Agent: pricing-agent]:::agent A --> M[Merge: wait for all]:::check B --> M C --> M M --> X[Code: merge-all aggregation]:::agent M -. or .-> Y[AI Agent: synthesis-agent]:::agent
on_partial_failure: proceed-with-available → in the Merge node, enable Continue on failure and handle missing branches in the aggregation Code node.
on_partial_failure: abort → connect error outputs from all branches to a shared error handler; the first failure triggers the abort path.
3.5 iterative
Implement as a loop: an IF node checks the convergence condition and routes either back to the AI Agent (iterate) or forward (complete).
flowchart TD
I[Input / first iteration]:::term --> A[AI Agent: react-agent, iterations feed back here]:::agent
A --> C[Code: check convergence]:::agent
C --> Q{IF: confidence >= 0.85 OR status == complete}:::check
Q -- true --> N[Continue to next step]:::term
Q -. false: back to AI Agent, connect output to AI Agent input .-> A
The Code node tracking iterations:
const iterationsUsed = ($execution.customData.get('iterationsUsed') || 0) + 1;
const maxIterations = 10;
await $execution.customData.set('iterationsUsed', iterationsUsed);
if (iterationsUsed >= maxIterations) {
// on_max_iterations_exceeded: escalate
throw new Error('MAX_ITERATIONS_EXCEEDED');
}
return [{ json: { ...$json, iterations_used: iterationsUsed } }];3.6 consensus
Parallel AI Agent branches (one per voter), then a Code node that implements the quorum function.
flowchart TD I[Input]:::term --> A1[AI Agent: strategy-agent-1]:::agent I --> A2[AI Agent: strategy-agent-2]:::agent I --> A3[AI Agent: strategy-agent-3]:::agent I --> A4[AI Agent: strategy-agent-4]:::agent I --> A5[AI Agent: strategy-agent-5]:::agent A1 --> M[Merge: wait for all]:::check A2 --> M A3 --> M A4 --> M A5 --> M M --> Q[Code: quorum evaluation]:::agent
The quorum Code node:
const votes = $input.all().map(item => item.json.result.vote);
const required = 3;
const total = votes.length;
const approvals = votes.filter(v => v === 'approve').length;
const quorumAchieved = approvals >= required;
if (!quorumAchieved) {
throw new Error('QUORUM_NOT_ACHIEVED');
}
return [{ json: { quorum_achieved: `${approvals}-of-${total}`, votes } }];On on_quorum_failure: abort → throw an error; the error path assembles a failure outcome.
On on_quorum_failure: escalate → route to the HITL sub-workflow.
3.7 hierarchical
Use Execute Workflow nodes for each sub-workflow. The wait_for: completion pattern uses synchronous Execute Workflow. wait_for: none uses asynchronous Execute Workflow (fire-and-forget mode).
flowchart TD P[Parent workflow trigger]:::term --> A[Execute Workflow: WF-COM-002-KYC-CHECK, wait_for: completion]:::agent A --> B[Execute Workflow: WF-COM-003-ACCOUNT-PROVISION, wait_for: completion, depends_on: KYC]:::agent B --> C[Execute Workflow: WF-COM-004-WELCOME-NOTIFICATION, wait_for: none, async]:::agent
The sub-workflow workflow_id in the contract corresponds to the workflow name or ID in n8n’s Execute Workflow node configuration. Use the Synedrix OS workflow_id tag (e.g., WF-COM-002-KYC-CHECK) to locate the sub-workflow.
Pass correlation_id and causation_id to each sub-workflow as input fields. The sub-workflow’s CORRELATION ID INIT Code node must inherit rather than generate a new correlation_id when one is already present.
3.8 pipeline
Linear chain of deterministic and agent-driven step nodes. Each step’s output JSON is the next step’s input.
flowchart TD A[Set: schema-normalise, step_type: deterministic]:::agent --> B[AI Agent: enrichment-agent, step_type: agent-driven]:::agent B --> C[Code: schema-validate, step_type: deterministic]:::agent C --> D[HTTP Request: publish-event, step_type: deterministic]:::agent
In a pipeline, deterministic steps must not alter the correlation_id or causation_id fields as they pass data forward. The Set node should use Keep all values and only add/modify the relevant transformation fields.
3.9 reflect-and-revise
AI Agent (producer) → Code node (quality evaluation) → IF (threshold met?) → either continue or loop back to AI Agent.
flowchart TD
I[Input]:::term --> G[AI Agent: generator-agent, producer_agent_role, iterations feed back here]:::agent
G --> Q[Code: evaluate quality — checks quality_threshold.metric against minimum]:::agent
Q --> F{IF: confidence_score >= 0.85}:::check
F -- true --> N[Continue to next workflow step]:::term
F -- false --> E[AI Agent: evaluator-agent, critic_agent_role]:::agent
E --> A[Code: attach critique_notes to input]:::agent
A --> B[back to generator AI Agent]:::agent
B -.-> G
The critique_output_field: critique_notes value is injected into the generator agent’s System Message on the next iteration:
// Code node: attach critique_notes
const critiqueNotes = $json.result.critique_notes;
const previousInput = $execution.customData.get('generatorInput');
return [{
json: {
...previousInput,
critique_notes: critiqueNotes,
revision_round: ($execution.customData.get('revisionRound') || 0) + 1
}
}];4. Step type → n8n node type reference
step_type | transform value | n8n node type | Notes |
|---|---|---|---|
deterministic | schema-validate | n8n-nodes-base.code | JavaScript/Python validation logic |
deterministic | schema-normalise / format-convert | n8n-nodes-base.set | Data reshaping; use Keep all values + overrides |
deterministic | data-join / merge-inputs | n8n-nodes-base.merge | Merge multiple input branches |
deterministic | filter-records | n8n-nodes-base.filter | Filter arrays by condition |
deterministic | publish-event / call-webhook | n8n-nodes-base.httpRequest | POST to event bus or Hookdeck Outpost |
deterministic | db-write (Supabase) | n8n-nodes-base.supabase | Insert/upsert to Supabase PG17 |
deterministic | db-read (Supabase) | n8n-nodes-base.supabase | Select from Supabase PG17 |
deterministic | redis-read / redis-write | n8n-nodes-base.redis | Read/write hot state |
deterministic | custom-logic | n8n-nodes-base.code | Arbitrary JS/Python with no LLM call |
deterministic | execute-sub-workflow | n8n-nodes-base.executeWorkflow | Used in hierarchical pattern |
agent-driven | (any) | @n8n/n8n-nodes-langchain.agent | Always requires Chat Model sub-node |
Naming rule for deterministic nodes: Use the format [{step_id}] {transform} as the node display name. Example: [step-1] schema-validate.
Naming rule for agent-driven nodes: Use the format [{LAYER}] {agent_id} · {name} (see §1.1).
5. Outcome envelope assembly
The Outcome Envelope (defined in Outcome Spec) must be assembled by a Code node placed immediately after each AI Agent node. The Code node is named [{step_id}] assemble-outcome.
5.1 Agent outcome assembly (Code node)
// [step-N] assemble-outcome
// Placed after an AI Agent node. Wraps the agent's output in the canonical outcome envelope.
const { randomUUID } = require('crypto');
const correlationId = $execution.customData.get('correlationId');
const causationId = $execution.customData.get('causationId');
const startedAt = $execution.customData.get('stepStartedAt') || $execution.customData.get('workflowStartedAt');
const completedAt = new Date().toISOString();
const durationMs = new Date(completedAt) - new Date(startedAt);
const agentOutput = $input.first().json;
// Determine status from agent output — convention: agent must set output.status or default to success
const outputStatus = agentOutput.error ? 'failure' : 'success';
const outcome = {
outcome_id: randomUUID(),
outcome_version: '1.0',
status: outputStatus,
correlation_id: correlationId,
causation_id: causationId,
source: {
type: 'agent',
id: 'REPLACE_WITH_AGENT_ID', // hardcode the agent_id for this node
version: 'REPLACE_WITH_VERSION', // hardcode the agent spec version
domain: 'REPLACE_WITH_DOMAIN'
},
result: outputStatus === 'success' ? (agentOutput.output || agentOutput) : null,
quality: agentOutput.quality || null,
errors: agentOutput.error
? [{ code: agentOutput.error.code || 'AGENT_ERROR', message: agentOutput.error.message, recoverable: false }]
: [],
escalation: null,
policy: agentOutput.policy || null,
timing: {
started_at: startedAt,
completed_at: completedAt,
duration_ms: Math.round(durationMs)
},
execution_context: null
};
return [{ json: outcome }];5.2 Workflow outcome assembly (Code node at workflow end)
// [workflow-end] assemble-workflow-outcome
const { randomUUID } = require('crypto');
const correlationId = $execution.customData.get('correlationId');
const causationId = $execution.customData.get('causationId');
const startedAt = $execution.customData.get('workflowStartedAt');
const completedAt = new Date().toISOString();
const durationMs = new Date(completedAt) - new Date(startedAt);
const workflowResult = $input.first().json;
const outcome = {
outcome_id: randomUUID(),
outcome_version: '1.0',
status: workflowResult.partial_failure_count > 0 ? 'partial' : 'success',
correlation_id: correlationId,
causation_id: causationId,
source: {
type: 'workflow',
id: 'REPLACE_WITH_WORKFLOW_ID', // e.g., WF-COM-001-PRODUCT-CREATION
version: 'REPLACE_WITH_VERSION',
domain: 'REPLACE_WITH_DOMAIN'
},
result: workflowResult,
quality: null,
errors: workflowResult.errors || [],
escalation: null,
policy: workflowResult.policy || null,
timing: {
started_at: startedAt,
completed_at: completedAt,
duration_ms: Math.round(durationMs)
},
execution_context: {
execution_pattern: 'REPLACE_WITH_PATTERN', // e.g., fan-out-fan-in
parallel_task_count: workflowResult.parallel_task_count || null,
partial_failure_count: workflowResult.partial_failure_count || null,
sub_workflow_outcomes: workflowResult.sub_workflow_outcomes || null,
quorum_achieved: workflowResult.quorum_achieved || null,
branches_taken: workflowResult.branches_taken || null
}
};
return [{ json: outcome }];5.3 Persisting the outcome
Persist each agent invocation in this order using Supabase nodes:
- Agent invocation start: Before execution, INSERT immutable identity and start-time fields into
agent_invocations, using a stable tenant-scopedidempotency_key. On conflict, verify and reuse the existing start row; never update it. Do not usecorrelation_idfor deduplication because multiple invocations may share it. - Tool call: Before dispatching each tool, INSERT its intent row into
agent_tool_call_records(unique per(tenant_id, invocation_id, hop_index); on conflict verify and reuse the row, passing itsidto the tool as an idempotency token where supported). Dispatch only after that insert commits. When the tool returns, INSERT its singleagent_tool_call_resultsrow (ON CONFLICT DO NOTHING, then verify the existing row, so a retry after a lost commit acknowledgement is safe) before returning the result to the agent or proceeding to another tool call. If a retry finds an existing intent with no result, do not re-dispatch a tool that lacks an idempotency token: mark the callunknownand escalate it for reconciliation. A crashed call leaves an intent row with no result, which the recovery sweep closes asunknown. Do not buffer tool-call audit records in Redis. - Agent outcome: After assembling the terminal envelope, INSERT exactly one row into
agent_invocation_outcomeskeyed by(tenant_id, invocation_id). On an idempotent replay, verify the existing outcome rather than updating it. - Workflow outcome: INSERT into
workflow_outcomes
The orchestration layer creates and persists the invocation start row, including its runtime_policy_version and deadline_at, before handing invocation_id to either the n8n AI Agent node or the LangGraph runtime (the invocation_id is a field of the LangGraph request and State contract in the LangGraph Runtime Implementation Guide §2). A LangGraph runtime writes the intent and result rows for its own tool calls under that invocation_id and returns the completed envelope; it does not write a second start row. n8n persists the terminal envelope in full, as the outcome_envelope of the outcome row. A recovery sweep inserts a timeout outcome for any start row past its own stored deadline_at plus grace that still has no outcome.
Both tables are append-only. Never use UPDATE or upsert on these tables.
5.4 Emitting the outcome event
After persisting, emit the corresponding os.outcome.* event via an HTTP Request node to the Hookdeck Outpost or internal event bus:
// Minimal outcome event payload (not the full outcome — only the fields the observability layer needs)
const outcomeEvent = {
event_type: `os.outcome.${outcome.status}`, // e.g., os.outcome.succeeded
correlation_id: outcome.correlation_id,
outcome_id: outcome.outcome_id,
source: outcome.source,
status: outcome.status,
duration_ms: outcome.timing.duration_ms,
emitted_at: new Date().toISOString()
};6. Policy gates and approval routing
6.1 Policy Guard gate (pre-execution)
When authority.approval_requirements.policy: true on an agent, or when a workflow step requires policy evaluation:
flowchart TD
U[Upstream step]:::term --> E[Execute Workflow: Policy Guard, WF-OS-POLICY-GATE or direct AI Agent call]:::agent
E --> S{Switch: on policy.decision}:::check
S -- allow --> C[Continue to agent-driven step]:::agent
S -- escalate --> H[HITL approval sub-workflow]:::agent
S -- block --> D[Dead Letter: assemble blocked outcome]:::warn
The Policy Guard outcome’s result.decision field drives the Switch node. When decision: block, the dead-letter path assembles an outcome with status: blocked.
6.2 HITL approval gate
When authority.approval_requirements.hitl: true:
flowchart TD
P[Policy Guard: decision = escalate]:::check --> E[Execute Workflow: HITL Gateway Agent, os-agt-004]:::agent
E --> W[Wait node: webhook-based, waits for human approval callback]:::agent
W --> S{Switch: on approval_status}:::check
S -- approved --> C[Continue to agent-driven step]:::agent
S -- rejected --> D[Dead Letter: assemble escalated outcome]:::warn
The Wait node URL is configured as a unique webhook path per execution (/hitl-callback/{{ $execution.id }}). The HITL Gateway Agent stores this callback URL in the escalation.hitl_request_id field of the outcome envelope.
Timeout on Wait node: configure to match the agent’s task_surface.task_timeout_ms. On timeout, route to the escalation path and emit status: timeout.
6.3 Tool-level approval gate
When a specific tool has approval_tier: policy in tool_permissions, wrap the tool sub-node invocation with a policy check before the AI Agent executes that specific tool. In practice, this means intercepting the AI Agent’s tool call output via a Code node and conditionally allowing or blocking the tool execution.
7. Memory profile → n8n memory node mapping
| Memory scope | Memory sub-node | Configuration |
|---|---|---|
working_memory: buffer-window | Buffer Window Memory (@n8n/n8n-nodes-langchain.memoryBufferWindow) | Context window size = working_memory_ttl_ms analog; typically 10–20 messages |
working_memory: postgres | Postgres Chat Memory (@n8n/n8n-nodes-langchain.memoryPostgresChat) | Supabase PG17 connection; session ID = correlation_id |
canonical_memory_access: read | Tool sub-node: Code tool → Supabase SELECT | Not a memory sub-node; exposed as a tool the agent calls explicitly |
semantic_memory_access: read | Tool sub-node: HTTP Request tool → Qdrant REST API | POST /collections/{name}/points/search with vector query |
graph_memory_access: read | Tool sub-node: HTTP Request tool → Neo4j Bolt API | Cypher query via HTTP; read-only |
temporal_memory_access: read | Tool sub-node: HTTP Request tool → QuestDB REST | SQL query via /exec?query=... |
Only one memory sub-node can be attached to an AI Agent node. Agents that need both working memory and long-term retrieval should use:
- Buffer Window Memory sub-node for in-session context
- Explicit tool sub-nodes for canonical/semantic/graph lookups
8. n8n workflow naming
Follow the Naming Convention Spec §5 for all n8n workflow display names.
Workflow display name format
[{LAYER}] {Layer tag} · {Business name}Examples:
[OS] L04 · Policy Guard Workflow
[TRD] L06 · Signal Ingestion Pipeline
[TRD] L05 · Risk Manager VETONode display name formats
| Node type | Format | Example |
|---|---|---|
| AI Agent node | [{LAYER}] {agent_id} · {name} | [OS] os-agt-002 · Policy Guard |
| Deterministic step node | [{step_id}] {transform} | [step-1] schema-validate |
| Outcome assembly Code node | [{step_id}] assemble-outcome | [step-3] assemble-outcome |
| Correlation ID Code node | [init] correlation-id | [init] correlation-id |
| Execute Workflow node | [sub] {workflow_id} | [sub] WF-COM-002-KYC-CHECK |
| Merge node | [merge] {description} | [merge] fan-in-results |
| Policy gate Switch node | [gate] policy-decision | [gate] policy-decision |
| HITL Wait node | [wait] hitl-approval | [wait] hitl-approval |
| Outcome persist node | [persist] {table-name} | [persist] workflow_outcomes |
| Event emit node | [emit] {event_type} | [emit] os.outcome.succeeded |
9. Compliance checklist
Before declaring an n8n workflow compliant with a Synedrix OS contract, verify:
- Workflow display name follows naming convention
-
correlation_idis initialized at workflow entry and passed to every Supabase write and event emit -
causation_idis initialized and set correctly (inheriting from trigger event or generating from execution ID) - Every AI Agent node has a Chat Model sub-node and a System Message that includes the agent’s
base_prompt_refcontent - Tool sub-nodes match the agent’s
tool_permissions(no extra tools, no missing required tools) - The
active_tool_setfrom the runtime policy’stool_authorization_policyhas been applied (unauthorized tools omitted) - Every AI Agent node is followed by an
assemble-outcomeCode node - Each agent invocation has a start row in
agent_invocations, tool-call intents inagent_tool_call_recordswith results inagent_tool_call_results, and one terminal row inagent_invocation_outcomes - Workflow outcomes are persisted to
workflow_outcomesvia a Supabase node - An
os.outcome.*event is emitted after outcome persistence - The workflow’s
execution_patternis logged in the workflow outcome’sexecution_context.execution_patternfield - Error paths produce a
failureorescalatedoutcome, not a silent empty result - HITL Wait nodes have a timeout configured matching
task_surface.task_timeout_ms - Deterministic step nodes do not invoke any LLM endpoint
Versioning
- Version: 0.3
- Supersedes: v0.2
Related Documents
Core Specs (what this guide implements)
- Synedrix OS - Agent Spec — Agent anatomy; all sub-objects mapped to n8n in §1 of this guide.
- Synedrix OS Workflow Contract Spec — Workflow contract fields and step types; §2–§4 of this guide implement these.
- Synedrix OS Outcome Spec — Outcome envelope; §5 of this guide provides the assembly Code node patterns.
- Synedrix OS - Agent Runtime Policy Spec — Runtime policy and tool authorization; §1.3–§1.4 map these to n8n node configuration.
Supporting Specs
- Synedrix OS Naming Convention Spec — Authoritative naming rules for n8n workflow and node display names.
- Synedrix OS Interoperability Protocol Strategy — MCP strategy; MCP Client Tool node in §1.4 implements this at the tool level.
- Synedrix OS - Agent DB Schema — Database schema; invocation starts, tool calls, and agent outcomes are persisted separately.
Synedrix OS · May 2026