Skip to content

Private Beta

Building, testing, and publishing agents are only available to a limited set of developers at this time.
The following documentation provides an early preview of the SDK, and the specifics are subject to change.

Example agents

The code samples below show various ways you can build agents and the features they can use.

💵 Currency converter

Converts an amount of money from one currency to another, using the ExchangeRate API to get the latest exchange rate. Has a skill that looks for placeholders in their writing and fills them in automatically.

import * as coda from "@codahq/packs-sdk";
export const pack = coda.newPack();

pack.addSkill({
  name: "PlaceholderFiller",
  displayName: "Placeholder Filler",
  description: 
    "Finds placeholders for currency values in the writing, and fills them in.",
  prompt: `
    Look for text like "$100 (or X CAD)" in the user's writing.
    Use the ExchangeRate tool to convert from one currency to another.
    Create a suggestion using the rewrite tool to replace the placeholder with 
    the value.
  `,
  tools: [
    { type: coda.ToolType.Pack },
    {
      type: coda.ToolType.ScreenAnnotation,
      annotation: { type: coda.ScreenAnnotationType.Rewrite },
    },
  ],
});

pack.addFormula({
  name: "ExchangeRate",
  description: "Gets the current exchange rate between two currencies.",
  parameters: [
    coda.makeParameter({
      type: coda.ParameterType.String,
      name: "from",
      description: "The ISO 4217 country code to convert from.",
    }),
    coda.makeParameter({
      type: coda.ParameterType.String,
      name: "to",
      description: "The ISO 4217 country code to convert to.",
    }),
  ],
  resultType: coda.ValueType.Number,
  execute: async function (args, context) {
    let [fromCountry, toCountry] = args;
    let response = await context.fetcher.fetch({
      method: "GET",
      url: `https://v6.exchangerate-api.com/v6/latest/${fromCountry}`,
    });
    // The JSON response is automatically parsed into an object.
    let rates = response.body.conversion_rates;
    let rate = rates[toCountry];
    if (!rate) {
      throw new coda.UserVisibleError("Exchange rate not available.");
    }
    return rate;
  },
});

// Use the same API key for all users, passed in the Authorization header.
pack.setSystemAuthentication({
  type: coda.AuthenticationType.HeaderBearerToken,
  instructionsUrl: "https://app.exchangerate-api.com/dashboard",
});

pack.addNetworkDomain("exchangerate-api.com");

💎 Google Gemini

An agent that passes all messages to Google Gemini for a reply. To verify that Gemini, and not the built-in LLM, is answering a question, it answers in rhyme and appends a gemstone emoji (💎) to each reply.

This pattern can be helpful if you want to develop an agent on your own infrastructure and expose it in Grammarly.

import * as coda from "@codahq/packs-sdk";
export const pack = coda.newPack();

const GeminiModel = "gemini-2.0-flash";

pack.addSkill({
  name: "GenerateReply",
  displayName: "Generate a reply.",
  description: "Generates a reply to the user.",
  prompt: `
    Always start by calling the GetGeminiReply tool.
    - In the messages parameter, pass all previous messages with role=user or 
      role=assistant, except those that have "editorText" in the ID.
    - In the screenContent parameter, pass the content of the most recent 
      message that has role=user and has "editorText" in the ID.
    - In the userContext parameter, pass the user context and timezone.
    Output the tool output exactly, including emojis, minus any surrounding 
    quotes. Don't output anything except for the tool output.

    ## Example
    System prompt: 
      The following JSON object describes the user asking the question: 
      {"name":"John Doe","email":"john@example.com"}
      Today's date is Wed, Oct 15, 2025, GMT-04:00. 
      Show all dates and times in the 'America/New_York' timezone.
    Message history: [
      { "role": "user", "content": "Cat", "id": "msg_editorText_abc" },
      { "role": "user", "content": "Hello" },
      { "role": "assistant", "content": "Howdy John" },
      { "role": "user", "content": "Dog", "id": "msg_editorText_abc" },
      { "role": "user", "content": "Goodbye" },
    ]
    Tool call: GetGeminiReply({
      messages: ["user:Hello", "model:Howdy John", "user:Goodbye"],
      screenContext: "Dog",
      userContext: "{
        \"name\":\"John Doe\",
        \"email\":\"john@example.com\", 
        \"timezone\": \"America/New_York\"
      }",
    })
    Tool output: "Bye John"
    Agent output: Bye John
  `,
  tools: [
    // All the formulas in this Pack.
    { type: coda.ToolType.Pack },
  ],
});

pack.setSkillEntrypoints({
  defaultChat: { skillName: "GenerateReply" },
});

pack.addFormula({
  name: "GetGeminiReply",
  description: "Passes a message to Gemini and gets a reply.",
  parameters: [
    coda.makeParameter({
      type: coda.ParameterType.StringArray,
      name: "messages",
      description: `
        The messages in the chat history, as an array of strings. Prefix each 
        message with either 'user:' or 'model:', depending on the source.
      `,
    }),
    coda.makeParameter({
      type: coda.ParameterType.String,
      name: "screenContext",
      description: "Context about what is on the user's screen.",
      optional: true,
    }),
    coda.makeParameter({
      type: coda.ParameterType.String,
      name: "userContext",
      description: `
        Context about the user that comes from the system prompt, such as 
        their name and email address.
      `,
      optional: true,
    }),
  ],
  resultType: coda.ValueType.String,
  execute: async function (args, context) {
    let [messages, screenContext, userContext] = args;
    let payload = {
      contents: messages.map(message => {
        // Break apart the role and message.
        let [_, role, text] = message.match(/(?:(\w+)\s*\:\s*)?(.*)/);
        return {
          role: role,
          parts: [{ text: text }],
        };
      }),
      system_instruction: {
        parts: [
          {
            text: `
              Append a space and the gem stone emoji (code point U+1F48E) 
              to every reply.

              ## Use this context to answer the question

              #### Screen context
              ${screenContext}

              #### User context
              ${userContext}
            `,
          },
        ],
      },
    };
    let response = await context.fetcher.fetch({
      method: "POST",
      url: "https://generativelanguage.googleapis.com/v1beta/models/" +
        `${GeminiModel}:generateContent`,
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(payload),
    });
    let reply = response.body.candidates[0].content.parts
      .map(p => p.text).join("");
    return reply;
  },
});

pack.setSystemAuthentication({
  type: coda.AuthenticationType.CustomHeaderToken,
  headerName: "X-goog-api-key",
  instructionsUrl: "https://aistudio.google.com/app/apikey",
});

pack.addNetworkDomain("googleapis.com");

✅ Todoist

An agent that allows you to work with your tasks in the app Todoist. It indexes all your tasks for semantic search and provides actions to create new tasks or mark them as done.

import * as coda from "@codahq/packs-sdk";
export const pack = coda.newPack();

pack.addNetworkDomain("todoist.com");

pack.setUserAuthentication({
  type: coda.AuthenticationType.OAuth2,
  // OAuth2 URLs and scopes are found in the the Todoist OAuth guide:
  // https://developer.todoist.com/guides/##oauth
  authorizationUrl: "https://todoist.com/oauth/authorize",
  tokenUrl: "https://todoist.com/oauth/access_token",
  scopes: ["data:read_write"],
  scopeDelimiter: ",",

  // Determines the display name of the connected account.
  getConnectionName: async function (context) {
    let response = await context.fetcher.fetch({
      method: "GET",
      url: "https://api.todoist.com/api/v1/user",
    });
    return response.body.full_name;
  },
});

const TaskSchema = coda.makeObjectSchema({
  properties: {
    name: {
      description: "The name of the task.",
      type: coda.ValueType.String,
      fromKey: "content",
    },
    description: {
      description: "A detailed description of the task.",
      type: coda.ValueType.String,
      codaType: coda.ValueHintType.Markdown,
    },
    url: {
      description: "A link to the task in the Todoist app.",
      type: coda.ValueType.String,
      codaType: coda.ValueHintType.Url,
    },
    priority: {
      description: "The priority of the task.",
      type: coda.ValueType.String,
      codaType: coda.ValueHintType.SelectList,
      options: ["P1", "P2", "P3", "P4"],
    },
    due: {
      description: "When the task is due.",
      type: coda.ValueType.String,
      codaType: coda.ValueHintType.DateTime,
    },
    id: {
      description: "The ID of the task.",
      type: coda.ValueType.String,
      required: true,
    },
  },
  displayProperty: "name",
  // Sync table metadata.
  idProperty: "id",
  featuredProperties: ["url", "priority", "due"],
  // Indexing metadata.
  titleProperty: "name",
  linkProperty: "url",
  index: {
    properties: ["description"],
    filterableProperties: ["priority"],
  },
});

// Index tasks into the knowledge layer.
pack.addSyncTable({
  name: "Tasks",
  schema: TaskSchema,
  identityName: "Task",
  formula: {
    name: "SyncTasks",
    description: "Sync tasks",
    parameters: [
      coda.makeParameter({
        type: coda.ParameterType.String,
        name: "filter",
        description: "A supported filter string. See the Todoist help center.",
        optional: true,
      }),
    ],
    execute: async function (args, context) {
      let [filter] = args;
      let url = coda.withQueryParams("https://api.todoist.com/rest/v2/tasks", {
        filter: filter,
      });
      let response = await context.fetcher.fetch({
        method: "GET",
        url: url,
      });
      let rows = response.body.map(task => formatTaskForSchema(task));
      return {
        result: rows,
      };
    },
  },
});

pack.addFormula({
  name: "Task",
  description: "Gets a Todoist task by URL",
  parameters: [
    coda.makeParameter({
      type: coda.ParameterType.String,
      name: "url",
      description: "The URL of the task",
    }),
  ],
  resultType: coda.ValueType.Object,
  schema: TaskSchema,
  execute: async function (args, context) {
    let [url] = args;
    let taskId = extractTaskId(url);
    let response = await context.fetcher.fetch({
      method: "GET",
      url: `https://api.todoist.com/rest/v2/tasks/${taskId}`,
    });
    return formatTaskForSchema(response.body);
  },
});

pack.addFormula({
  name: "AddTask",
  description: "Add a new task. Returns the URL of the new task.",
  parameters: [
    coda.makeParameter({
      type: coda.ParameterType.String,
      name: "name",
      description: "The name of the task.",
    }),
    coda.makeParameter({
      type: coda.ParameterType.String,
      name: "description",
      description: "The description of the task.",
    }),
  ],
  resultType: coda.ValueType.String,
  isAction: true,
  execute: async function (args, context) {
    let [name, description] = args;
    let response = await context.fetcher.fetch({
      method: "POST",
      url: "https://api.todoist.com/rest/v2/tasks",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        content: name,
        description: description,
      }),
    });
    return response.body.url;
  },
});

pack.addFormula({
  name: "MarkComplete",
  description: "Mark a task as complete.",
  parameters: [
    coda.makeParameter({
      type: coda.ParameterType.String,
      name: "url",
      description: "The URL of the task",
    }),
  ],
  resultType: coda.ValueType.String,
  isAction: true,
  execute: async function (args, context) {
    let [url] = args;
    let taskId = extractTaskId(url);
    await context.fetcher.fetch({
      method: "POST",
      url: `https://api.todoist.com/rest/v2/tasks/${taskId}/close`,
    });
    return "OK";
  },
});

function extractTaskId(taskUrl: string) {
  let pattern = /^https:\/\/app\.todoist\.com\/app\/task\/(?:\w+-)*(\w+)$/;
  let matches = taskUrl.match(pattern);
  if (matches && matches[1]) {
    return matches[1];
  }
  throw new coda.UserVisibleError("Invalid task URL: " + taskUrl);
}

// Format a task from the API and return an object matching the Task schema.
function formatTaskForSchema(task: any) {
  return {
    ...task,
    // Convert the priority to a string like "P1".
    priority: "P" + (5 - task.priority),
  };
}

🅰️ Gen Alpha

An agent that suggests ways to incorporate more Gen Alpha slang into the user's writing.

import * as coda from "@codahq/packs-sdk";
export const pack = coda.newPack();

pack.addSkill({
  name: "GenAlpha",
  displayName: "Make Gen Alpha",
  description: "Make your writing use more Generation Alpha slang.",
  prompt: `
    You are a writing agent that specializes in Generation Alpha slang.
    Looks at the text the user is writing.
    Replace their words and phrases with the equivalent Gen Alpha slang.
    Use the Rewrite tool to make those suggestions.
    Only make a single call to the Rewrite tool, passing in all suggestions.
    Only pass in one rewrite per-paragraph, combining all the changes for that 
    paragraph.
    For each suggestion, include a very brief explanation (10 words or less).
  `,
  tools: [
    {
      type: coda.ToolType.ScreenAnnotation,
      annotation: { type: coda.ScreenAnnotationType.Rewrite },
    },
  ],
});

🧙 Gandalf

This example shows how you can override the defaultChat skill to build custom routing between the other skills in your Pack.

import * as coda from "@codahq/packs-sdk";
export const pack = coda.newPack();

pack.setSkillEntrypoints({
  defaultChat: { skillName: "Gandalf" },
});

pack.addSkill({
  name: "Gandalf",
  displayName: "Gandalf",
  description: "Speak to Gandalf.",
  prompt: `
    If the user mentions a ring, transfer to the Secret skill.
    If the user mentions Durin, transfer to the Friend skill.
    Otherwise respond back with the phrase "You shall not pass!".
  `,
  tools: [],
});

pack.addSkill({
  name: "Secret",
  displayName: "Secret",
  description: "Talk about secrets.",
  prompt: `
    Reply with "Keep it secret, keep it safe".
  `,
  tools: [],
});

pack.addSkill({
  name: "Friend",
  displayName: "Friend",
  description: "Talk about friends.",
  prompt: `
    Reply with "Speak, friend and enter".
  `,
  tools: [],
});

🪙 Coin flip

Demonstrates how to handle a long-running process by using one formula to start the process and another to check the status, run in a loop by the LLM.

import * as coda from "@codahq/packs-sdk";
export const pack = coda.newPack();

// How long to keep checking the flip status in a single tool call.
// The LLM will chain together multiple tool calls.
const MaxRuntimeSeconds = 30;

// How long to pause between checking the flip results.
const SleepTimeSeconds = 5;

pack.setSkillEntrypoints({
  defaultChat: { skillName: "FlipCoin" },
});

pack.addSkill({
  name: "FlipCoin",
  displayName: "Flip a coin",
  description: "Flips a coin and gets the result.",
  prompt: `
    1. If not provided, as the user how many seconds to flip the coin for.
    2. Call StartCoinFlip to begin the coin flip.
    3. Call GetResult to check the status of the flip, and possibly get the 
       result.
    4. If the flip isn't complete (isComplete is false), go back to step 3 and 
       call GetResult again.
    5. Tell the user the result of the coin flip. Only do this if the flip is 
       complete. If not, always call GetResult again.

    Don't stop until you have a result. 
    You must get a result, no matter how long it takes.
  `,
  tools: [
    {
      type: coda.ToolType.Pack,
    },
  ],
});

const FlipSchema = coda.makeObjectSchema({
  properties: {
    isComplete: { type: coda.ValueType.Boolean },
    result: { type: coda.ValueType.String },
  },
  displayProperty: "result",
});

pack.addFormula({
  name: "StartCoinFlip",
  description: "Starts a coin flip. Returns the ID of the flip.",
  parameters: [
    coda.makeParameter({
      type: coda.ParameterType.Number,
      name: "seconds",
      description: "How long the coin should flip for, in seconds.",
    }),
  ],
  resultType: coda.ValueType.String,
  cacheTtlSecs: 0,
  execute: async function (args, context) {
    let [seconds] = args;
    let flipId = startFlip(seconds);
    return flipId;
  },
});

pack.addFormula({
  name: "GetResult",
  description: "Gets the result of a coin flip.",
  parameters: [
    coda.makeParameter({
      type: coda.ParameterType.String,
      name: "flipId",
      description: "The ID of the coin flip.",
    }),
  ],
  resultType: coda.ValueType.Object,
  schema: FlipSchema,
  cacheTtlSecs: 0,
  execute: async function (args, context) {
    let [flipId] = args;
    let start = new Date();
    let max = start.getTime() + (MaxRuntimeSeconds * 1000);
    let result;
    do {
      if (result) {
        // Not the first time through the loop, pause for a few seconds.
        await Atomics.wait(
          new Int32Array(
            new SharedArrayBuffer(4)), 0, 0, SleepTimeSeconds * 1000);
      }
      result = await getResult(flipId);
    } while (!result.isComplete && Date.now() < max);
    return result;
  },
});

function startFlip(seconds: number) {
  // In a real agent this would make a request to an API and get back an ID.
  // In this example, we just encode the end time in the ID itself.
  let now = new Date();
  let end = new Date(now.getTime() + (seconds * 1000));
  return Buffer.from(end.toUTCString()).toString("base64");
}

function getResult(flipId: string) {
  // In a real agent this would make a request to get the status / result.
  // In this example, we just decode the end time from the ID.
  let end = new Date(Buffer.from(flipId, "base64").toString());
  let now = new Date();
  if (now > end) {
    return {
      isComplete: true,
      result: Math.random() > 0.5 ? "heads" : "tails",
    };
  } else {
    return {
      isComplete: false,
    };
  }
}