Trigger.dev recipe
Receive Cortex triggers in a Trigger.dev task. You get durable execution + checkpoint-resume + retry/backoff on your side for free; Cortex handles dispatch, signing, and the callback contract.
Why Trigger.dev
- TypeScript-first, durable task runtime — survives timeouts, network blips, process restarts.
- Native MCP server support in 2026 (Trigger.dev exposes itself as MCP-callable; orthogonal to this recipe but useful for hybrid setups).
- Apache 2.0 + self-hostable; ~422K weekly npm downloads.
Architecture
A Trigger.dev HTTP-triggered task verifies the signature, runs the action via your channel, and POSTs trigger.complete back to Cortex. If anything fails, Trigger.dev's retry budget kicks in; if it persists, Cortex's outer DLQ eventually catches it.
Verification (canonical)
typescriptcortex-dispatch.ts — Trigger.dev task
import { task } from "@trigger.dev/sdk/v3";
import { createHmac, timingSafeEqual } from "node:crypto";
const WEBHOOK_SECRET = process.env.CORTEX_WEBHOOK_SECRET!;
const PRINCIPAL_TOKEN = process.env.CORTEX_PRINCIPAL_TOKEN!;
const CORTEX_HOST = "https://mcp.cortex.jakeselby.com";
export const cortexDispatch = task({
id: "cortex-dispatch",
// Trigger.dev's HTTP trigger: this task is invoked by an HTTP POST.
// (See Trigger.dev docs for the exact http-trigger wiring; the task body is what changes.)
run: async ({
rawBody,
headers,
}: {
rawBody: string;
headers: Record<string, string>;
}) => {
const sig = (headers["x-cortex-signature"] ?? "").replace(/^sha256=/, "");
const ts = headers["x-cortex-timestamp"] ?? "";
const expected = createHmac("sha256", WEBHOOK_SECRET)
.update(ts + "." + rawBody, "utf8")
.digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(sig, "hex");
if (a.length !== b.length || !timingSafeEqual(a, b)) {
throw new Error("signature_invalid");
}
const trigger = JSON.parse(rawBody) as {
trigger_id: string;
action_class: string;
mode: "act" | "notify_after" | "ask" | "test";
counterparty?: { id: string; display_name: string };
payload: { body?: string };
};
// Run the action through your own channel — Trigger.dev handles retries here.
const externalRef = await sendThroughYourChannel(trigger);
// Close the loop
await fetch(`${CORTEX_HOST}/v1/triggers/${trigger.trigger_id}/complete`, {
method: "POST",
headers: {
Authorization: `Bearer ${PRINCIPAL_TOKEN}`,
"Content-Type": "application/json",
"Idempotency-Key": `cb_${trigger.trigger_id}`,
},
body: JSON.stringify({
outcome: trigger.mode === "ask" ? "approved" : "executed",
external_ref: externalRef,
executed_at: new Date().toISOString(),
}),
});
return { ok: true, external_ref: externalRef };
},
});
async function sendThroughYourChannel(_trigger: unknown): Promise<string> {
// Your channel send. Slack, email, in-app, SMS, anything.
return "msg_" + Math.random().toString(36).slice(2);
}Register the handler with Cortex
Once your task is deployed and exposed at an HTTPS URL, register it once:
bash
curl -s https://mcp.cortex.jakeselby.com/mcp \
-H "Authorization: Bearer $CORTEX_PRINCIPAL_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{
"name":"handler.register",
"arguments":{
"url":"https://your-trigger-project.example.com/api/cortex",
"action_classes":["communicate","schedule"],
"webhook_secret_alias":"trigger-dev-prod"
}
}
}'
# Persist the returned webhook_secret as CORTEX_WEBHOOK_SECRET in your Trigger.dev env.Verify end-to-end
Run a synthetic dispatch with handler.test and watch the Trigger.dev run dashboard for the task execution.