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_pattern values to n8n workflow topology
  • Mapping from step_type to 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 conceptn8n construct
Agent (agent-driven step)AI Agent node (@n8n/n8n-nodes-langchain.agent)
prompt_policy.base_prompt_refAI Agent node → System Message field
runtime_policy.primary_modelChat Model sub-node (OpenAI, Anthropic, Ollama)
tool_permissions entryTool sub-node connected to AI Agent node
MCP toolMCP Client Tool node (@n8n/n8n-nodes-langchain.mcpClientTool)
memory_profile.working_memoryBuffer Window Memory sub-node
memory_profile.canonical_memory_accessSupabase Postgres Chat Memory or Code tool
Deterministic stepNative n8n node (Set, Code, Merge, Filter, HTTP Request)
execution_pattern: sequentialLinear node chain
execution_pattern: parallelParallel branches → Merge node
execution_pattern: conditionalIF node or Switch node
execution_pattern: fan-out-fan-inParallel branches → Merge → Code (aggregation)
execution_pattern: iterativeIF node loop back to AI Agent
execution_pattern: consensusParallel AI Agent branches → Code (quorum evaluation)
execution_pattern: hierarchicalExecute Workflow node
execution_pattern: pipelineLinear Set/Code/AI Agent chain
execution_pattern: reflect-and-reviseAI Agent → Code (quality check) → IF → loop or exit
Outcome envelopeCode node after AI Agent node
correlation_id$execution.customData.correlationId (set at workflow entry)
Policy gateExecute Workflow (Policy Guard) → IF node on decision
HITL approvalExecute Workflow (HITL) → Wait node → webhook callback
Workflow outcomeCode 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}
Illustrative example, not a production implementation. Review and test before use.

Examples:

[OS] os-agt-002 · Policy Guard
[TRD] trd-agt-001 · Signal Ingestion
[TRD] trd-ctl-risk-manager-001 · Risk Manager VETO
Illustrative example, not a production implementation. Review and test before use.

This 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 fieldn8n AI Agent node field
prompt_policy.base_prompt_refSystem Message (primary content)
prompt_policy.role_prompt_refAppended to System Message as a role block
prompt_policy.safety_overlay_refAppended to System Message as a safety block
prompt_policy.domain_overlay_refAppended to System Message (domain context)
prompt_policy.tenant_overlay_refInjected at runtime via expression in System Message
prompt_policy.environment_overlay_refInjected 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 }}
Illustrative example, not a production implementation. Review and test before use.

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 fieldn8n Chat Model sub-node
primary_model: gpt-4oOpenAI Chat Model node → model: gpt-4o
primary_model: claude-sonnet-4-6Anthropic Chat Model node → model: claude-sonnet-4-6
primary_model: ollama/llama3.1:8bOllama Chat Model node → model: llama3.1:8b, base URL: http://localhost:11434
primary_model: vllm/mistralai/Mistral-7BHTTP-based Chat Model (custom) pointing at vLLM endpoint
model_type: hybridTwo 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 fieldMaps to
tool_idTool node display name: tool:{tool_id}
access_mode: readTool node configuration restricted to read operations only
access_mode: read-writeTool node with full operation scope
side_effect_class: writeTool node must be wrapped with Policy Guard call when approval_tier requires it
approval_tier: policyn8n sub-workflow: call Policy Guard Agent before tool execution
approval_tier: hitln8n Wait node: pause execution, call HITL Gateway Agent
audit_mode: fullEmit agent.tool.authorized event after tool call (Code node)

Tool sub-node types by tool category:

Tool categoryn8n 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 readCustom 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 fieldn8n 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: readCode tool wrapping a Supabase SELECT
semantic_memory_access: readCode tool making HTTP GET to Qdrant /collections/{name}/points/search
graph_memory_access: readCode tool making HTTP POST to Neo4j Bolt API
temporal_memory_access: readCode tool making HTTP GET to QuestDB REST SQL endpoint
audit_memory_access: readNot 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 fieldn8n equivalent
workflow_idWorkflow Tags: add wf_id:{workflow_id} tag
nameWorkflow display name (follows naming convention §5 of Naming Spec)
trigger_type: eventWebhook trigger node or Hookdeck → n8n webhook
trigger_type: scheduleSchedule trigger node
trigger_type: human-commandManual trigger or form trigger
trigger_type: api-callWebhook trigger node
trigger_type: callbackWebhook trigger node (awaiting external callback)
idempotency_strategyGenerate a stable invocation idempotency key; correlation_id groups work but is not unique
retry_policyn8n workflow error workflow + retry configuration
error_policy.escalation_targetError workflow: Execute Workflow node to escalation sub-workflow
versionWorkflow 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 } }];
Illustrative example, not a production implementation. Review and test before use.

Every subsequent node accesses correlation ID via:

const correlationId = $execution.customData.get('correlationId');
Illustrative example, not a production implementation. Review and test before use.

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 } }];
Illustrative example, not a production implementation. Review and test before use.

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 } }];
Illustrative example, not a production implementation. Review and test before use.

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
  }
}];
Illustrative example, not a production implementation. Review and test before use.

4. Step type → n8n node type reference

step_typetransform valuen8n node typeNotes
deterministicschema-validaten8n-nodes-base.codeJavaScript/Python validation logic
deterministicschema-normalise / format-convertn8n-nodes-base.setData reshaping; use Keep all values + overrides
deterministicdata-join / merge-inputsn8n-nodes-base.mergeMerge multiple input branches
deterministicfilter-recordsn8n-nodes-base.filterFilter arrays by condition
deterministicpublish-event / call-webhookn8n-nodes-base.httpRequestPOST to event bus or Hookdeck Outpost
deterministicdb-write (Supabase)n8n-nodes-base.supabaseInsert/upsert to Supabase PG17
deterministicdb-read (Supabase)n8n-nodes-base.supabaseSelect from Supabase PG17
deterministicredis-read / redis-writen8n-nodes-base.redisRead/write hot state
deterministiccustom-logicn8n-nodes-base.codeArbitrary JS/Python with no LLM call
deterministicexecute-sub-workflown8n-nodes-base.executeWorkflowUsed in hierarchical pattern
agent-driven(any)@n8n/n8n-nodes-langchain.agentAlways 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 }];
Illustrative example, not a production implementation. Review and test before use.

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 }];
Illustrative example, not a production implementation. Review and test before use.

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-scoped idempotency_key. On conflict, verify and reuse the existing start row; never update it. Do not use correlation_id for 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 its id to the tool as an idempotency token where supported). Dispatch only after that insert commits. When the tool returns, INSERT its single agent_tool_call_results row (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 call unknown and escalate it for reconciliation. A crashed call leaves an intent row with no result, which the recovery sweep closes as unknown. Do not buffer tool-call audit records in Redis.
  • Agent outcome: After assembling the terminal envelope, INSERT exactly one row into agent_invocation_outcomes keyed 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()
};
Illustrative example, not a production implementation. Review and test before use.

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 scopeMemory sub-nodeConfiguration
working_memory: buffer-windowBuffer Window Memory (@n8n/n8n-nodes-langchain.memoryBufferWindow)Context window size = working_memory_ttl_ms analog; typically 10–20 messages
working_memory: postgresPostgres Chat Memory (@n8n/n8n-nodes-langchain.memoryPostgresChat)Supabase PG17 connection; session ID = correlation_id
canonical_memory_access: readTool sub-node: Code tool → Supabase SELECTNot a memory sub-node; exposed as a tool the agent calls explicitly
semantic_memory_access: readTool sub-node: HTTP Request tool → Qdrant REST APIPOST /collections/{name}/points/search with vector query
graph_memory_access: readTool sub-node: HTTP Request tool → Neo4j Bolt APICypher query via HTTP; read-only
temporal_memory_access: readTool sub-node: HTTP Request tool → QuestDB RESTSQL 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}
Illustrative example, not a production implementation. Review and test before use.

Examples:

[OS] L04 · Policy Guard Workflow
[TRD] L06 · Signal Ingestion Pipeline
[TRD] L05 · Risk Manager VETO
Illustrative example, not a production implementation. Review and test before use.

Node display name formats

Node typeFormatExample
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_id is initialized at workflow entry and passed to every Supabase write and event emit
  • causation_id is 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_ref content
  • Tool sub-nodes match the agent’s tool_permissions (no extra tools, no missing required tools)
  • The active_tool_set from the runtime policy’s tool_authorization_policy has been applied (unauthorized tools omitted)
  • Every AI Agent node is followed by an assemble-outcome Code node
  • Each agent invocation has a start row in agent_invocations, tool-call intents in agent_tool_call_records with results in agent_tool_call_results, and one terminal row in agent_invocation_outcomes
  • Workflow outcomes are persisted to workflow_outcomes via a Supabase node
  • An os.outcome.* event is emitted after outcome persistence
  • The workflow’s execution_pattern is logged in the workflow outcome’s execution_context.execution_pattern field
  • Error paths produce a failure or escalated outcome, 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

Core Specs (what this guide implements)

Supporting Specs



Synedrix OS · May 2026