Super Advanced · Lesson 22 of 22

Custom Packs: SDK, Auth & Publishing

No existing Pack for your internal tool? Build your own. A custom Pack adds reusable formulas, sync tables, and button actions to any doc — then ships as a versioned module you can share privately or publish to 300,000+ Coda users.

⏱ ~50 min ⚙️ TypeScript / JavaScript ✅ Prerequisite: Lesson 21
01 — Why Build a Pack 02 — SDK Overview 03 — Your First Formula 04 — Sync Tables 05 — Authentication 06 — Custom Actions 07 — CLI Development 08 — Publishing & Versioning 09 — Course Complete Practice
01 — Why Build a Custom Pack

When existing Packs don't cover your stack.

Coda's Pack gallery covers hundreds of popular tools — Salesforce, Jira, GitHub, Stripe. But some integrations need to be custom-built: your internal systems, proprietary APIs, or complex logic that's too unique to exist in a published Pack.

🔌

Internal tool integration

Your company's internal API doesn't have a public Pack. Build one and your whole team can sync data from it into any doc — no code required on their end.

♻️

Reusable complex logic

A formula or transformation too complex to write inline in every doc. Package it as a Pack formula — write it once, use it everywhere, update it in one place.

🌍

Publish to the gallery

Once your Pack is production-ready, publish it to the Coda gallery. Over 300,000 Coda users can install and use it — from anyone in your workspace to the entire community.

What a Pack can add to any doc Custom formulas (callable like built-in Coda formulas), sync tables (live-syncing rows from an external API), and button actions (POST requests or complex operations triggered by a button column). All bundled, versioned, and installable in one click.
02 — Pack SDK Overview

TypeScript, two dev environments, one deployment target.

The Pack SDK is a TypeScript/JavaScript library — @codahq/packs-sdk on npm. You write your Pack logic in TypeScript, and the SDK handles the runtime environment, authentication plumbing, and deployment to Coda's Pack infrastructure.

🌐

Pack Studio (Browser IDE)

Built into Coda. No npm, no install. Open any doc → "+" → Build a Pack. Editor, test console, and deploy button all in one browser window. Best for getting started and simple Packs.

💻

CLI (Local Development)

npm install, VS Code, git, unit tests, multiple files. Best for complex Packs, team collaboration, and when you need TypeScript autocomplete and automated test coverage.

Start with Pack Studio, graduate to CLI Pack Studio's zero-setup experience is perfect for prototyping and learning. Once your Pack has more than ~100 lines or needs proper test coverage, move to the CLI for version control and the full TypeScript development experience.
03 — Your First Pack Formula

Write it in Pack Studio, call it like a native formula.

Open any Coda doc → "+" → Build a Pack. The Pack Studio editor opens. Your Pack starts with a basic scaffold. Add your first formula using pack.addFormula() — it becomes callable in any table or canvas in that doc immediately after you save.

Pack Studio — First Formula: Greet(name)
import * as coda from "@codahq/packs-sdk";
export const pack = coda.newPack();

pack.addFormula({
  name: "Greet",
  description: "Returns a personalized greeting",
  parameters: [
    coda.makeParameter({
      type: coda.ParameterType.String,
      name: "name",
      description: "The name to greet",
    }),
  ],
  resultType: coda.ValueType.String,
  execute: async ([name]) => {
    return "Hello, " + name + "!";
  },
});

Click Save in Pack Studio, then switch to a table or canvas in the doc. Type =Greet("World") — it returns "Hello, World!" just like a built-in Coda formula. The formula is callable from any cell, formula column, or automation in the doc.

execute() is async by default The execute function is always async — even for trivial formulas. This is because any real-world formula might make an API call, and the SDK treats all formulas uniformly. You can use await inside execute() for any async operation.
04 — Sync Tables

Pull external API data into a live Coda table.

A sync table is the Pack feature that creates a Coda table whose rows come from an external API. When a user syncs the table, your execute() function runs, fetches fresh data, and Coda updates the rows. The table stays live — users can re-sync on demand or on a schedule.

Two things define a sync table: (1) a schema — the columns the table will have, and (2) an execute() function — the async function that fetches data and returns rows matching that schema.

Sync Table — Pull Posts from JSONPlaceholder API
const PostSchema = coda.makeObjectSchema({
  properties: {
    title:  { type: coda.ValueType.String },
    body:   { type: coda.ValueType.String },
    userId: { type: coda.ValueType.Number },
    id:     { type: coda.ValueType.Number, fromKey: "id" },
  },
  displayProperty: "title",
  idProperty: "id",
});

pack.addSyncTable({
  name: "Posts",
  schema: PostSchema,
  identityName: "Post",
  formula: {
    name: "SyncPosts",
    description: "Sync posts from the API",
    parameters: [],
    execute: async ([], context) => {
      const url = "https://jsonplaceholder.typicode.com/posts";
      const response = await context.fetcher.fetch({ method: "GET", url });
      const posts = response.body;
      return { result: posts };
    },
  },
});

Pagination in sync tables

If the API returns paginated results, return a continuation object alongside results. Coda calls execute() again with that continuation until there's nothing left to fetch.

Sync Table Pagination — continuation Pattern
// In execute(), check for more pages
const page = context.sync.continuation?.page || 1;
const response = await context.fetcher.fetch({
  method: "GET",
  url: `https://api.example.com/items?page=${page}`
});
const hasMore = response.body.hasNextPage;

return {
  result: response.body.items,
  continuation: hasMore ? { page: page + 1 } : undefined,
};
05 — Authentication

Per-user credentials — three patterns.

If your API requires authentication, you declare the auth method in your Pack and Coda handles credential collection. Each Coda user who installs the Pack enters their own credentials — the Pack never sees anyone else's keys. Three patterns cover nearly all APIs:

🔓

No auth

Public APIs — no setup needed. Your execute() function just fetches the URL. Best for read-only public data sources.

🔑

API Key (Bearer token)

User enters their API key when installing the Pack. Coda injects it as a Bearer token on every fetch call automatically.

Authentication — API Key (HeaderBearerToken)
// API key injected as Bearer token on every request
pack.setUserAuthentication({
  type: coda.AuthenticationType.HeaderBearerToken,
  instructionsUrl: "https://docs.yourapi.com/authentication",
});
Authentication — OAuth2
// OAuth2 — Coda handles the full authorize/callback/token flow
pack.setUserAuthentication({
  type: coda.AuthenticationType.OAuth2,
  authorizationUrl: "https://app.example.com/oauth/authorize",
  tokenUrl: "https://app.example.com/oauth/token",
  scopes: ["read", "write"],
});
Per-user, not per-Pack Authentication in Packs is per-user. Each person who installs your Pack authenticates with their own credentials. This means Coda never shares credentials between users — and you never have to manage a shared service account token.
06 — Custom Actions

Button columns that call your API.

Actions are formulas with isAction: true. When a user creates a button column and selects a Pack action, Coda runs that execute() function when the button is clicked. The parameters become configurable fields in the button's settings — users can wire in thisRow values so each row triggers the action with its own data.

Custom Action — CreateTicket (POST to internal system)
pack.addFormula({
  name: "CreateTicket",
  description: "Creates a support ticket in the internal system",
  isAction: true,
  parameters: [
    coda.makeParameter({ type: coda.ParameterType.String, name: "title",
      description: "Ticket title" }),
    coda.makeParameter({ type: coda.ParameterType.String, name: "priority",
      description: "High, Medium, or Low" }),
  ],
  resultType: coda.ValueType.String,
  execute: async ([title, priority], context) => {
    const response = await context.fetcher.fetch({
      method: "POST",
      url: "https://internal.company.com/api/tickets",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ title, priority }),
    });
    return "Ticket created: #" + response.body.id;
  },
});

In a table, add a button column. Set the formula to =CreateTicket([Task Name], [Priority]). When someone clicks the button in a row, the action fires with that row's Task Name and Priority values — one click creates the ticket in your system and returns the confirmation ID.

07 — CLI Development & Testing

Local development with git, tests, and TypeScript.

When your Pack outgrows Pack Studio — more than a few files, complex business logic, team collaboration, or CI/CD requirements — move to the CLI. The CLI gives you the full TypeScript development experience: VS Code autocomplete, unit tests with Jest, git history, and PRs before deploying.

CLI — Setup, Test, and Deploy
# Install the Packs CLI globally
npm install -g @codahq/packs-sdk

# Scaffold a new Pack project
coda init my-pack
# Creates: pack.ts, package.json, tsconfig.json, .gitignore

# Test a formula locally without uploading
coda execute pack.ts Greet '"World"'
# → "Hello, World!"

# Run unit tests (Jest)
npm test

# Upload to Coda (creates a new version)
coda upload pack.ts
🧪

Local testing

coda execute runs any formula or sync table locally with real API calls. Test without deploying to Coda at all — your iteration cycle is seconds, not minutes.

📁

Multiple files / modules

The CLI lets you split your Pack across multiple TypeScript files — separate files for schemas, helpers, auth config, and each major feature. Pack Studio requires everything in one file.

08 — Publishing & Versioning

Private, workspace, or 300,000 users in the gallery.

Packs use semantic versioning. Declare the version with pack.setVersion("1.0.0"). Every upload creates a new version — previous versions remain available so users aren't broken by updates. Write release notes so users understand what changed.

🔒

Private (workspace only)

Available only to your workspace. No review required. Share via the Pack's settings → "Share with workspace." Best for internal tools.

🌍

Public gallery

Submit for Coda's quality and safety review. Once approved, your Pack appears in the gallery for 300,000+ users to install. Review typically takes 1–2 weeks.

Versioning rules Patch (1.0.0 → 1.0.1): bug fixes, no schema changes. Minor (1.0.0 → 1.1.0): new columns added to sync tables, new formulas. Major (1.0.0 → 2.0.0): breaking changes — removing columns from a sync table, renaming existing formulas. Breaking changes require a major bump because users' existing docs break.
Setting Pack Version
export const pack = coda.newPack();
pack.setVersion("1.2.0");

// Release notes appear in the Pack's update history
// users see what changed when they upgrade
09 — Course Complete

You've mastered Coda — from zero to custom Packs.

Congratulations. You've covered the full Coda spectrum — from your first doc to publishing code that runs inside Coda's infrastructure. Here's what you've built:

📗

Beginner (Lessons 1–6)

Docs, tables, column types, views, filtering, sorting. The foundation every Coda user needs before anything else.

📘

Intermediate (Lessons 7–12)

Formulas, relation columns, rollup logic, automations, and the Pack ecosystem. Where Coda goes from "fancy spreadsheet" to connected system.

📙

Advanced (Lessons 13–19)

Canvas pages, forms, Coda AI columns, and the REST API. Reading and writing Coda tables programmatically from external code.

📕

Super Advanced (Lessons 20–22)

Full CRM, full PM system, and custom Packs. Production-grade systems that integrate with the rest of your stack.

What to build next

Your capstone project Combine the CRM from Lesson 20 and the PM system from Lesson 21 — then write a custom Pack that integrates your team's unique internal tool. That's the full stack: a relational data model, live automation, AI columns, an API integration, and a custom Pack formula that no one else has built. That's a real Coda power user.
Practice

Test your knowledge.

Lesson 22 Quiz

5 Questions
Question 1 of 5
What is the key difference between Pack Studio and the Pack CLI?
✓ Pack Studio requires no install and runs in the browser — perfect for learning and simple Packs. The CLI adds git, unit tests, multiple file modules, and TypeScript autocomplete for more complex development.
Question 2 of 5
What is the purpose of the schema in a sync table definition?
✓ The schema defines the column structure of the sync table — property names, types, the display column, and the ID property. Every row returned by execute() must match this schema.
Question 3 of 5
With OAuth2 authentication in a Pack, whose credentials are used when a user calls a Pack formula?
✓ Authentication in Packs is per-user. Each person who installs the Pack connects their own account. When they call a formula, Coda uses their credentials — not a shared account. This is a security feature, not a limitation.
Question 4 of 5
What does isAction: true do when added to an addFormula() definition?
✓ isAction: true marks a formula as a button action. It appears in the button column's formula picker. When the button is clicked, execute() runs — typically to POST data to an API or perform an operation with side effects.
Question 5 of 5
What type of version bump is required when you remove a column from a sync table's schema?
✓ Removing a column from a sync table is a breaking change — any formula, view filter, or automation in a user's doc that references that column will break. Breaking changes require a major version bump (e.g., 1.0.0 → 2.0.0).
← Previous Building a Project Management System Lesson 21