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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
await inside execute() for any async operation.
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.
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 }; }, }, });
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.
// 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, };
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:
Public APIs — no setup needed. Your execute() function just fetches the URL. Best for read-only public data sources.
User enters their API key when installing the Pack. Coda injects it as a Bearer token on every fetch call automatically.
// API key injected as Bearer token on every request pack.setUserAuthentication({ type: coda.AuthenticationType.HeaderBearerToken, instructionsUrl: "https://docs.yourapi.com/authentication", });
// 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"], });
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.
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.
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.
# 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
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.
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.
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.
Available only to your workspace. No review required. Share via the Pack's settings → "Share with workspace." Best for internal tools.
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.
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
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:
Docs, tables, column types, views, filtering, sorting. The foundation every Coda user needs before anything else.
Formulas, relation columns, rollup logic, automations, and the Pack ecosystem. Where Coda goes from "fancy spreadsheet" to connected system.
Canvas pages, forms, Coda AI columns, and the REST API. Reading and writing Coda tables programmatically from external code.
Full CRM, full PM system, and custom Packs. Production-grade systems that integrate with the rest of your stack.