Recipes

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.