n8n recipe
Receive Cortex triggers in an n8n workflow. Self-hosted or n8n Cloud; visual editor with a Webhook trigger, a Function node for HMAC verification, a channel node (Slack / Email / HTTP Request / anything in n8n's catalog), and an HTTP Request node for trigger.complete.
Why n8n
- Visual workflow editor — non-engineers can extend the handler.
- Self-hostable; Sustainable Use License (n8n is broadly self-host-friendly).
- ~400+ built-in nodes for downstream channels.
Workflow shape
- Webhook node — HTTP method POST, response: “Respond Immediately” with status code 200. (Cortex needs the ack in ≤30s; we'll process async on the workflow side.)
- Function node — Verify HMAC (snippet below). On signature mismatch, throw to short-circuit the workflow.
- Channel node — Slack, Gmail, Twilio, in-app HTTP, whatever your app uses.
- HTTP Request node — trigger.complete — POST back to Cortex with outcome + external ref.
Function node — verify the signature
javascriptn8n Function node
// Inputs from the Webhook node:
// - $input.first().json.body (string) — needs Webhook node "Raw Body" option enabled
// - $input.first().json.headers (object)
const crypto = require('crypto');
const WEBHOOK_SECRET = $env.CORTEX_WEBHOOK_SECRET;
const FIVE_MIN = 5 * 60 * 1000;
const body = $input.first().binary?.data?.toString('utf8')
?? JSON.stringify($input.first().json.body);
const headers = $input.first().json.headers ?? {};
const sig = (headers['x-cortex-signature'] || '').replace(/^sha256=/, '');
const ts = headers['x-cortex-timestamp'] || '';
const tsMs = Date.parse(ts);
if (Number.isNaN(tsMs) || Math.abs(Date.now() - tsMs) > FIVE_MIN) {
throw new Error('stale_or_bad_timestamp');
}
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(ts + '.' + body, 'utf8')
.digest('hex');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(sig, 'hex');
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
throw new Error('signature_invalid');
}
// Pass the parsed trigger to downstream nodes
return [{ json: JSON.parse(body) }];HTTP Request node — trigger.complete
Configure as POST to:
text
https://mcp.cortex.jakeselby.com/v1/triggers/{{$json["trigger_id"]}}/completeHeaders:
text
Authorization: Bearer {{$env.CORTEX_PRINCIPAL_TOKEN}}
Idempotency-Key: cb_{{$json["trigger_id"]}}
Content-Type: application/jsonBody (JSON):
json
{
"outcome": "executed",
"external_ref": "{{$node[\"YourChannelNode\"].json[\"id\"]}}",
"executed_at": "{{$now.toISOString()}}"
}