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 Superhuman Go.

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

const GeminiModel = "gemini-2.0-flash";

pack.setChatSkill({
  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.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 uses the Todoist MCP server to perform CRUD operations, and the REST API to index tasks into the knowledge layer for semantic search.

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

// The REST API and MCP server are hosted on different domains.
// Note: Connecting to multiple domains requires approval.
pack.addNetworkDomain("todoist.com");
pack.addNetworkDomain("todoist.net");

// The REST API and MCP server use the same OAuth credentials.
pack.setUserAuthentication({
  type: coda.AuthenticationType.OAuth2,
  authorizationUrl: "https://todoist.com/oauth/authorize",
  tokenUrl: "https://todoist.com/oauth/access_token",
  scopes: ["data:read_write"],
  scopeDelimiter: ",",

  // Allow the credentials to be sent to both domains.
  networkDomain: ["todoist.com", "todoist.net"],

  // 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;
  },
});

pack.addMCPServer({
  name: "Todoist",
  endpointUrl: "https://ai.todoist.net/mcp",
});

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 => {
          return {
            ...task,
            // Convert the priority to a string like "P1".
            priority: "P" + (5 - task.priority),
          };
      });
      return {
        result: rows,
      };
    },
  },
});

🅰️ 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.setChatSkill({
  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, ask 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,
    };
  }
}

🔳 QR code generator

Demonstrates how to configure an agent to start running when its icon is clicked and utilize the web page URL available in the context.

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

pack.addNetworkDomain("api.qrserver.com");

// When the user clicks on the agent, immediately run this skill to generate a
// QR code using the URL of the current page.
pack.setBenchInitializationSkill({
  name: "CurrentPageQRCode",
  displayName: "Current Page QR Code",
  description: "Generate a QR containing the URL for the current page.",
  prompt: `
    Generate a QR code, using the URL of the current page as the text.
    Reply with a summary, and the resulting image rendered in markdown format.
  `,
  tools: [
    {
      type: coda.ToolType.Pack,
      formulas: [
        { formulaName: "QRCode" },
      ],
    },
  ],
});

pack.addFormula({
  name: "QRCode",
  description: "Generate a QR code for the text supplied.",
  parameters: [
    coda.makeParameter({
      type: coda.ParameterType.String,
      name: "text",
      description: "The text to encode in the QR code.",
    }),
  ],
  resultType: coda.ValueType.String,
  codaType: coda.ValueHintType.ImageReference,
  execute: async function (args, context) {
    let [text] = args;
    return coda.withQueryParams("https://api.qrserver.com/v1/create-qr-code/", {
      data: text,
      size: "512x512",
    });
  },
});

🗽 NY Senate

Demonstrates how to index PDF content and contacts.

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

export const pack = coda.newPack();

// How many members to sync in a single execution.
const MembersPageSize = 1000;

// How many documents to process in a single execution.
const DocumentsPageSize = 20;

// One hour in seconds, used for caching.
const OneHourSecs = 60 * 60;

// Allow requests to NY's Open Legislation API.
// https://legislation.nysenate.gov/static/docs/html/index.html
pack.addNetworkDomain("nysenate.gov");

// The API uses an API key in the query parameters.
pack.setSystemAuthentication({
  type: coda.AuthenticationType.QueryParamToken,
  paramName: "key",
});

// The schema for a member, with contact indexing.
const MemberSchema = coda.makeObjectSchema({
  description: "A member of the NY State Senate.",
  properties: {
    id: {
      type: coda.ValueType.String,
      fromKey: "memberId",
      description: "A unique ID for the member.",
    },
    name: {
      type: coda.ValueType.String,
      fromKey: "fullName",
      description: "The full name of the member.",
    },
    district: {
      type: coda.ValueType.Number,
      fromKey: "districtCode",
      description: "The congressional district that the member represents.",
    },
    email: {
      type: coda.ValueType.String,
      codaType: coda.ValueHintType.Email,
      description: "The email address of the member.",
    },
  },
  displayProperty: "name",
  idProperty: "id",
  featuredProperties: ["district", "email"],
  // Contact indexing.
  userEmailProperty: "email",
});

// The schema for a law.
const LawSchema = coda.makeObjectSchema({
  description: "A law in the New York State legal code.",
  properties: {
    id: {
      type: coda.ValueType.String,
      fromKey: "lawId",
      required: true,
      description: "A unique ID for the law.",
    },
    name: {
      type: coda.ValueType.String,
      required: true,
      description: "The name of the law.",
    },
    type: {
      type: coda.ValueType.String,
      fromKey: "lawType",
      description:
        "The type of law (Consolidated, Unconsolidated, Constitution, etc)",
    },
    chapter: {
      type: coda.ValueType.String,
      description: "The chapter designation for the law.",
    },
    link: {
      type: coda.ValueType.String,
      codaType: coda.ValueHintType.Url,
      description: "A link to the law on nysenate.gov.",
    },
  },
  displayProperty: "name",
  idProperty: "id",
  featuredProperties: ["type", "chapter", "link"],
});

// The schema for a document, with full-text indexing.
const DocumentSchema = coda.makeObjectSchema({
  properties: {
    id: {
      type: coda.ValueType.String,
      description: "A unique ID for the document.",
    },
    title: {
      type: coda.ValueType.String,
      description: "The title of the document.",
    },
    path: {
      type: coda.ValueType.String,
      description: `
        A breadcrumb trail of the parent documents (chapter, title, article,
        etc) that contain this document.
      `,
    },
    law: {
      ...coda.makeReferenceSchemaFromObjectSchema(LawSchema, "Law"),
      description: "The law that this document pertains to.",
    },
    pdf: {
      type: coda.ValueType.String,
      codaType: coda.ValueHintType.Attachment,
      description: "The contents of the document, as a PDF.",
    },
    link: {
      type: coda.ValueType.String,
      codaType: coda.ValueHintType.Url,
      description: "A link to the document on nysenate.gov.",
    },
    lastModified: {
      type: coda.ValueType.String,
      codaType: coda.ValueHintType.Date,
      description: "When the document was last modified.",
    },
  },
  displayProperty: "title",
  idProperty: "id",
  featuredProperties: ["law", "link"],
  // Configure indexing.
  titleProperty: "title",
  linkProperty: "link",
  modifiedAtProperty: "lastModified",
  versionProperty: "lastModified",
  index: {
    properties: ["pdf"],
    contextProperties: ["title", "path"],
  },
});

// A sync table of members.
pack.addSyncTable({
  name: "Members",
  description: "Lists the current members of the senate.",
  identityName: "Member",
  schema: MemberSchema,
  formula: {
    name: "SyncMembers",
    description: "Syncs the data.",
    parameters: [],
    execute: async function (args, context) {
      let offset = context.sync.continuation?.offset as number ?? 0;

      // Get the current year in New York.
      let year = new Intl.DateTimeFormat("en-US", {
        year: "numeric",
        timeZone: "America/New_York",
      }).format(new Date());

      let url = coda.withQueryParams(
        `https://legislation.nysenate.gov/api/3/members/${year}`,
        {
          full: true,
          limit: MembersPageSize,
          offset: offset,
        }
      );
      let response = await context.fetcher.fetch({
        method: "GET",
        url: url,
        cacheTtlSecs: OneHourSecs,
      });
      let data = response.body;
      let rows = data.result.items.map(member => {
        return {
          ...member,
          email: member.person.email,
        };
      });
      return {
        result: rows,
      };
    },
  },
});

// A sync table of laws.
pack.addSyncTable({
  name: "Laws",
  description: "Lists the laws on the books.",
  instructions: `
    This table contains basic metadata for the laws. Use the Documents table to
    get the text of the laws.
  `,
  identityName: "Law",
  schema: LawSchema,
  formula: {
    name: "SyncLaws",
    description: "Syncs the data.",
    parameters: [],
    execute: async function (args, context) {
      let response = await context.fetcher.fetch({
        method: "GET",
        url: "https://legislation.nysenate.gov/api/3/laws",
      });
      let data = response.body;
      let rows = data.result.items.map(law => {
        return {
          ...law,
          link: `https://www.nysenate.gov/legislation/laws/${law.lawId}`,
        };
      });
      return {
        result: rows,
      };
    },
  },
});

// A sync table of documents.
pack.addSyncTable({
  name: "Documents",
  description: "",
  identityName: "Document",
  schema: DocumentSchema,
  formula: {
    name: "SyncDocuments",
    description: "Syncs the data.",
    parameters: [
      coda.makeParameter({
        type: coda.ParameterType.String,
        name: "law",
        description: "The ID of the law.",
        autocomplete: async function (context, search) {
          let response = await context.fetcher.fetch({
            method: "GET",
            url: "https://legislation.nysenate.gov/api/3/laws",
            cacheTtlSecs: OneHourSecs,
          });
          let data = response.body;
          let laws = data.result.items;
          return coda.autocompleteSearchObjects(search, laws, "name", "lawId");
        },
        // During indexing, run a sync for each law in the Laws table.
        crawlStrategy: {
          parentTable: {
            tableName: "Laws",
            propertyKey: "id",
          },
        },
      }),
    ],
    execute: async function (args, context) {
      let [lawId] = args;
      let start = context.sync.continuation?.start as number ?? 0;

      // Fetch the document tree related to a law.
      let response = await context.fetcher.fetch({
        method: "GET",
        url: `https://legislation.nysenate.gov/api/3/laws/${lawId}`,
        cacheTtlSecs: OneHourSecs,
      });
      let data = response.body;
      let root = data.result.documents;

      // Get a list of leaf documents within the tree (non-leaf documents are
      // just indices). Adds a `parents` field to the object with the array of
      // parent documents.
      let documents = getLeafDocuments(root);

      // Trim to one page of documents (we can't process all the PDFs in a
      // single execution).
      let batch = documents.slice(start, start + DocumentsPageSize);

      // Format the document into a row.
      let rows = batch.map(document => {
        let documentId = getDocumentId(document);
        return {
          id: documentId,
          name: document.title,
          number: document.docLevelId,
          title: getFormattedTitle(document),
          path: document.parents?.map(
            parent => getFormattedTitle(parent)).join(" >"),
          law: {
            lawId: lawId,
            name: document.lawName,
          },
          // The PDF contents of the document is accessed via an API endpoint.
          /* eslint-disable max-len */
          pdf: `https://legislation.nysenate.gov/pdf/laws/${documentId}?full=true`,
          link: `https://www.nysenate.gov/legislation/laws/${document.lawId}/${document.locationId}`,
          /* eslint-enable max-len */
          lastModified: document.activeDate,
        };
      });

      // Copy the PDFs to temporary blob storage, which is required for them to
      // be indexed.
      await Promise.all(rows.map(async row => {
        let tempUrl = await context.temporaryBlobStorage.storeUrl(row.pdf, {
          downloadFilename: `${row.id}.pdf`,
        });
        // Replace the API-provided URL with the new temp URL.
        row.pdf = tempUrl;
      }));

      // If there are more documents to process for this law, make a
      // continuation with the next start index.
      let continuation;
      if (documents.length > start + DocumentsPageSize) {
        continuation = { start: start + DocumentsPageSize };
      }

      return {
        result: rows,
        continuation: continuation,
      };
    },
  },
});

// Given a node in the document tree, returns an array of leaf documents.
function getLeafDocuments(document, parents?) {
  if (document.documents.size === 0) {
    // This is a leaf document, return it.
    document.parents = parents;
    return [document];
  }
  if (!parents) {
    parents = [];
  }
  // Add the current document to the current list of parents.
  parents = [...parents, document];
  let children = document.documents.items;
  // Recursively call this function on the children and return the merged array.
  return children.map(child => getLeafDocuments(child, parents)).flat();
}

// Get a unique ID for a document, which can also be used by the PDF endpoint.
function getDocumentId(document) {
  return `${document.lawId}${document.locationId}`;
}

// Get a formatted title of the document, which returns the type and identifier.
// E.g. "Section 74: Use of the great seal"
function getFormattedTitle(document) {
  let { docType, docLevelId, title } = document;
  return `${toTitleCase(docType)} ${docLevelId}: ${title}`;
}

// Converts a word to title case. E.g. "cat" => "Cat".
function toTitleCase(str) {
  return str.slice(0, 1).toUpperCase() + str.slice(1).toLowerCase();
}