How a Coda Pack Calls Grok Bot

Packs can POST to a Grok Bot webhook, send Coda data with the task, and get an HTTP status back.

Coda can call Grok Bot. A Pack action POSTs JSON to a Cursor webhook. That wakes a bot on the shared computer. You can include Coda data in the payload — row fields, selections, a JSON blob from a formula — so the bot is not guessing what you meant. It gets the instruction and the context from the doc.

That is the real story. Not “Coda becomes an agent platform.” Not “rebuild your stack in Pack Studio.” A thin call-out: task a bot, and attach the Coda data it needs.

It’s not obvious that Grok Bot routines also include an inbound webhook option. Home automation is one example. So is summarizing a table, filing a brief, or kicking a deeper run that uses tools already installed on the Grok Bot computer. Same knock. Different jobs.

Figure 1 — Coda Pack formulas POST to a Cursor webhook with a task and optional Coda data, wake the shared Grok Bot computer, and return an HTTP status while the bot continues the work.

The Pack

I built a small Pack called bot-pack with two actions: SendToGrokBot and SendToGrokBotJson.

SendToGrokBot takes content (the task or prompt) and optional data. It sends content, source: “coda”, and sentAt. If data is a JSON object, those fields merge into the body — that is how you ship Coda context (ids, selected rows, labels, whatever your routine expects). Other values ride under data.

SendToGrokBotJson is for when you already built a full JSON object in the doc. Pass that object string. Arrays and primitives fail with a clear error.

Both authenticate with Pack system auth as HeaderBearerToken. After upload, paste the webhook sender key (crsr_…, not the word Bearer) into Pack Studio system authentication. Coda adds Authorization: Bearer on the fetch.

Scrubbed sample — replace the webhook id:

//
// bot-pack, by Bill French
//

import * as coda from "@codahq/packs-sdk";

export const pack = coda.newPack();

const GROK_BOT_WEBHOOK_URL =
  "https://api2.cursor.sh/automations/webhook/<YOUR-WEBHOOK-ID>";

pack.addNetworkDomain("api2.cursor.sh");

// Coda injects `Authorization: Bearer <token>` from system credentials.
// After upload: Pack Studio → Settings → Add system authentication → paste the
// `crsr_…` token (not the "Bearer " prefix).
pack.setSystemAuthentication({
  type: coda.AuthenticationType.HeaderBearerToken,
});

pack.addFormula({
  name: "SendToGrokBot",
  description:
    "Sends a content payload to the Cursor Grok Bot webhook. Use from a button or Coda automation.",
  isAction: true,
  parameters: [
    coda.makeParameter({
      type: coda.ParameterType.String,
      name: "content",
      description: "Primary message or prompt for the Grok Bot.",
      suggestedValue: "Hello from Coda",
    }),
    coda.makeParameter({
      type: coda.ParameterType.String,
      name: "data",
      description:
        "Optional extra payload. JSON objects are merged into the body; other values are sent as `data`.",
      optional: true,
    }),
  ],
  resultType: coda.ValueType.String,
  examples: [
    { params: ["Summarize this week's tasks"], result: "HTTP 200" },
  ],
  execute: async function ([content, data], context) {
    return postToGrokBot(context, buildContentPayload(content, data));
  },
});

pack.addFormula({
  name: "SendToGrokBotJson",
  description:
    "Sends a raw JSON object to the Cursor Grok Bot webhook. Use when you already have a structured payload.",
  isAction: true,
  parameters: [
    coda.makeParameter({
      type: coda.ParameterType.String,
      name: "json",
      description: "JSON object string to POST as the webhook body.",
    }),
  ],
  resultType: coda.ValueType.String,
  examples: [
    {
      params: ['{"content":"Ship the weekly brief"}'],
      result: "HTTP 200",
    },
  ],
  execute: async function ([json], context) {
    return postToGrokBot(context, parseJsonObject(json));
  },
});

function buildContentPayload(
  content: string,
  data?: string,
): Record<string, unknown> {
  const payload: Record<string, unknown> = {
    content,
    source: "coda",
    sentAt: new Date().toISOString(),
  };

  if (!data || !data.trim()) {
    return payload;
  }

  try {
    const parsed = JSON.parse(data);
    if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
      return {
        ...payload,
        ...(parsed as Record<string, unknown>),
        content,
      };
    }
    payload.data = parsed;
    return payload;
  } catch {
    payload.data = data;
    return payload;
  }
}

function parseJsonObject(json: string): Record<string, unknown> {
  let parsed: unknown;
  try {
    parsed = JSON.parse(json);
  } catch {
    throw new coda.UserVisibleError(
      "json must be a valid JSON object string.",
    );
  }

  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
    throw new coda.UserVisibleError(
      "json must be a JSON object, not an array or primitive.",
    );
  }

  return parsed as Record<string, unknown>;
}

async function postToGrokBot(
  context: coda.ExecutionContext,
  payload: Record<string, unknown>,
): Promise<string> {
  const response = await context.fetcher.fetch({
    method: "POST",
    url: GROK_BOT_WEBHOOK_URL,
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(payload),
  });

  return formatWebhookResult(response);
}

function formatWebhookResult(response: coda.FetchResponse): string {
  const body = formatBody(response.body);
  return body ? `HTTP ${response.status}: ${body}` : `HTTP ${response.status}`;
}

function formatBody(body: unknown): string {
  if (body == null || body === "") {
    return "";
  }
  if (typeof body === "string") {
    return body;
  }
  try {
    return JSON.stringify(body);
  } catch {
    return String(body);
  }
}

Task + Coda data

Wire a button. Put the instruction in content. Put doc context in data or in the JSON object for SendToGrokBotJson — row id, project name, selected text, a small table snapshot, whatever the bot needs to act without another round trip.

Example shape:

{
  "content": "Turn on the living room scene",
  "source": "coda",
  "sentAt": "2026-09-09T22:00:00.000Z",
  "room": "living",
  "scene": "evening"
}

Or:

{
  "content": "Draft a status note from these rows",
  "source": "coda",
  "docId": "…",
  "rows": [{ "title": "…", "owner": "…", "due": "…" }]
}

Same Pack. Same webhook. Different tasks. The point is you are not limited to a bare string — you can hand the bot structured Coda data with the ask.

What comes back

The Pack returns the webhook’s HTTP status (and body if present) — typically HTTP 200. It does not wait for a long agent run to finish and paste the full result into the button. Confirm the call, then let the bot work.

If you need a result back in the doc later, have the bot write a row, update a cell, or leave a status another action can read. That is separate from the call. Grok Bot can use Coda MCP, so responses, even hours later, are possible. I have several long-running agents, and this model works really well.

One example among many

Home automation is a clean demo: button → Pack → webhook → bot → Home Assistant. Useful, memorable, and not the thesis.

The thesis is: from Coda, you can task a Grok Bot and include Coda data in the payload. Everything else — lights, briefs, deeper CLI work on the bot computer — is an application of that one idea.

Why it matters

You already know Packs that call outside APIs. This is the same habit aimed at an agent computer that can keep working after the Pack returns. Trigger and context from Coda. Execution on Grok Bot.

Awareness first. Then build whatever jobs you actually need.

Thanks for sharing @Bill_French! I haven’t tried out Grok Bot yet, but this is good inspiration to do so.