--- name: coda-pack-dev description: Use when the user asks about building Coda Packs, creating formulas, sync tables, actions, authentication, schemas, parameters, or any Coda Pack SDK questions. --- # Coda Pack Development Expert Expert assistance for building Coda Packs using the Coda Pack SDK. Provides guidance on formulas, sync tables, actions, authentication, and best practices. ## What Are Coda Packs? Coda Packs are extensions that add new capabilities to Coda documents. They're built using TypeScript/JavaScript and run on Coda's servers as serverless applications. ### Four Core Extension Types 1. **Formulas** - Custom functions for calculations or data retrieval 2. **Actions** - Special formulas that modify external applications (used in buttons/automations) 3. **Column Formats** - Control how values display in tables 4. **Sync Tables** - Automated tables that pull data from external sources ## Pack Structure ### Basic pack.ts Template ```typescript import * as coda from "@codahq/packs-sdk"; export const pack = coda.newPack(); // Add network domains your pack will access pack.addNetworkDomain("api.example.com"); // Add formulas, sync tables, etc. ``` ## Creating Formulas ### Formula Anatomy ```typescript pack.addFormula({ name: "FormulaName", // Upper camel case, letters/numbers/underscores only description: "What it does", parameters: [ // Array of parameters coda.makeParameter({ type: coda.ParameterType.String, name: "paramName", description: "What this param does", optional: false, // Optional: mark as optional param }), ], resultType: coda.ValueType.String, // Return type isAction: false, // Set true for actions (buttons/automations) execute: async function(args, context) { // Your code here return result; }, }); ``` ### Parameter Types Common parameter types: - `ParameterType.String` - Text values - `ParameterType.Number` - Numeric values - `ParameterType.Boolean` - True/false - `ParameterType.Date` - Date values - `ParameterType.StringArray` - Array of strings - `ParameterType.NumberArray` - Array of numbers - `ParameterType.Html` - HTML content - `ParameterType.Image` - Image URL or data ### Result Types Common result types: - `ValueType.String` - Text - `ValueType.Number` - Numbers - `ValueType.Boolean` - True/false - `ValueType.Array` - Array of values - `ValueType.Object` - Structured object (requires schema) ### Formula Naming Best Practices - **Data sources**: Use plural nouns (`Tasks`, `Users`) - **Transformations**: Use verbs (`Reverse`, `Truncate`) - **Multiple words**: Upper camel case (`BugReport`, `DeletedFiles`) - **Avoid prefixes**: Skip "Get", "Lookup", "Query" - **Unique names**: Must be unique within pack ⚠️ **Warning**: Renaming formulas breaks existing documents using them! ### Making HTTP Requests ```typescript execute: async function([param1, param2], context) { let response = await context.fetcher.fetch({ method: "POST", url: "https://api.example.com/endpoint", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ key: "value", }), }); // Check status if (response.status !== 200) { throw new coda.UserVisibleError("API request failed"); } // Access response body let data = response.body; return data.result; } ``` ## Actions vs Regular Formulas **Actions** (`isAction: true`) are formulas designed for buttons and automations: - Can modify external systems (create, update, delete) - Show differently in the formula picker - Used in button formulas and automations **Regular formulas** are for calculations and data retrieval: - Should be idempotent (same inputs = same output) - Cached for performance - Used in table columns and document calculations ## Sync Tables ### Basic Sync Table Structure ```typescript // 1. Define schema const ItemSchema = coda.makeObjectSchema({ properties: { id: { type: coda.ValueType.Number, fromKey: "id", required: true, }, name: { type: coda.ValueType.String, fromKey: "name", required: true, }, url: { type: coda.ValueType.String, fromKey: "url", codaType: coda.ValueHintType.Url, }, }, displayProperty: "name", // Primary column idProperty: "id", // Unique identifier featuredProperties: ["name", "url"], // Show by default }); // 2. Add sync table pack.addSyncTable({ name: "Items", identityName: "Item", // Singular form schema: ItemSchema, formula: { name: "SyncItems", description: "Sync items from API", parameters: [ // Optional filter parameters ], execute: async function(args, context) { let response = await context.fetcher.fetch({ method: "GET", url: "https://api.example.com/items", }); let items = response.body.items; return { result: items, // Array of objects matching schema }; }, }, }); ``` ### Sync Table Key Concepts **Identity**: Every sync table needs `identityName` (singular form of table name) **Schema Properties**: Define the structure of each row - Use `fromKey` to map API field names to schema properties - Set `required: true` for mandatory fields - Use `displayProperty` for the primary column - Use `idProperty` for unique identification **Row Limits**: - Default: 1,000 rows - Maximum: 10,000 rows **Continuations**: For paginated results exceeding 60s timeout: ```typescript execute: async function(args, context) { let url = context.sync.continuation?.nextUrl || "https://api.example.com/items"; let response = await context.fetcher.fetch({ method: "GET", url }); let continuation; if (response.body.next_page) { continuation = { nextUrl: response.body.next_page, }; } return { result: response.body.items, continuation: continuation, }; } ``` ### Dynamic Sync Tables For APIs with multiple similar endpoints: ```typescript pack.addDynamicSyncTable({ name: "DynamicTable", getName: async function(context) { // Return table name based on context }, getSchema: async function(context) { // Return schema based on selected entity }, getDisplayUrl: async function(context) { // Return URL to view the data }, listDynamicUrls: async function(context) { // Return list of available tables }, formula: { // Same as regular sync table }, }); ``` **See [Advanced Sync Tables](references/advanced-sync-tables.md) for:** - Complete dynamic sync table implementation guide - Two-way sync (editable sync tables) - Property options and dynamic dropdowns - Folder organization, search, and manual URL entry ### Two-Way Sync Tables Enable users to edit synced data and push changes back to external sources. Mark columns as editable with `mutable: true` and implement an `executeUpdate` function: ```typescript const TaskSchema = coda.makeObjectSchema({ properties: { id: { type: coda.ValueType.Number, required: true }, name: { type: coda.ValueType.String, mutable: true, // Editable column }, status: { type: coda.ValueType.String, codaType: coda.ValueHintType.SelectList, options: ["Todo", "In Progress", "Done"], mutable: true, }, }, displayProperty: "name", idProperty: "id", }); pack.addSyncTable({ name: "Tasks", identityName: "Task", schema: TaskSchema, formula: { name: "SyncTasks", description: "Sync tasks", parameters: [], execute: async function([], context) { // Fetch data from API let response = await context.fetcher.fetch({ method: "GET", url: "https://api.example.com/tasks", }); return { result: response.body.tasks }; }, executeUpdate: async function([], context, updates) { // Push changes back to API let results = []; for (let update of updates) { let response = await context.fetcher.fetch({ method: "PATCH", url: `https://api.example.com/tasks/${update.previousValue.id}`, body: JSON.stringify({ name: update.newValue.name, status: update.newValue.status, }), }); results.push(response.body); } return { result: results }; }, }, }); ``` **See [Advanced Sync Tables](references/advanced-sync-tables.md) for:** - Property options (static and dynamic) - Batch updates with `maxUpdateBatchSize` - Error handling in updates - OAuth incremental authorization ## Authentication ### Setting Up Authentication ```typescript // User authentication (most common) pack.setUserAuthentication({ type: coda.AuthenticationType.HeaderBearerToken, instructionsUrl: "https://example.com/get-api-key", }); // Or system authentication (shared credentials) pack.setSystemAuthentication({ type: coda.AuthenticationType.HeaderBearerToken, }); ``` ### Authentication Types - **HeaderBearerToken** - Bearer token in Authorization header - **CustomHeaderToken** - Custom header name - **QueryParamToken** - Token in query string - **WebBasic** - Username and password - **OAuth2** - OAuth 2.0 flow - **CodaApiHeaderBearerToken** - For Coda API - **AWSAccessKey** - AWS Signature v4 ### OAuth2 Authentication ```typescript pack.setUserAuthentication({ type: coda.AuthenticationType.OAuth2, authorizationUrl: "https://example.com/oauth/authorize", tokenUrl: "https://example.com/oauth/token", scopes: ["read", "write"], // Get account name for the connection getConnectionName: async function(context) { let response = await context.fetcher.fetch({ method: "GET", url: "https://api.example.com/me", }); return response.body.username; }, }); ``` ### Per-Account Endpoints For services with account-specific domains: ```typescript pack.setUserAuthentication({ type: coda.AuthenticationType.HeaderBearerToken, // Prompt user for their account URL getConnectionName: async function(context) { return context.endpoint || "Unknown"; }, endpointDomain: async function(context) { return context.endpoint; }, networkDomain: "example.com", }); // Access endpoint in formulas execute: async function(args, context) { let url = `https://${context.endpoint}/api/endpoint`; // ... } ``` ## Schemas ### Object Schema ```typescript const PersonSchema = coda.makeObjectSchema({ properties: { id: { type: coda.ValueType.Number }, name: { type: coda.ValueType.String }, email: { type: coda.ValueType.String, codaType: coda.ValueHintType.Email, }, joinDate: { type: coda.ValueType.String, codaType: coda.ValueHintType.Date, }, avatar: { type: coda.ValueType.String, codaType: coda.ValueHintType.ImageReference, }, isActive: { type: coda.ValueType.Boolean }, }, displayProperty: "name", idProperty: "id", featuredProperties: ["name", "email"], }); ``` ### Value Hints (codaType) Make data more interactive: - `ValueHintType.Url` - Clickable links - `ValueHintType.Email` - Email addresses - `ValueHintType.Date` - Date formatting - `ValueHintType.DateTime` - Date and time - `ValueHintType.ImageReference` - Display images - `ValueHintType.ImageAttachment` - Inline images - `ValueHintType.Markdown` - Render markdown - `ValueHintType.Html` - Render HTML ### Array Schemas ```typescript // Array of objects const resultType = coda.ValueType.Array; const schema = coda.makeSchema({ type: coda.ValueType.Array, items: PersonSchema, // Schema for each item }); ``` ## Network Domains Whitelist domains your pack will access: ```typescript pack.addNetworkDomain("api.example.com"); pack.addNetworkDomain("cdn.example.com"); // Support subdomains pack.addNetworkDomain("example.com"); // Allows *.example.com ``` ## Error Handling ### User-Visible Errors ```typescript execute: async function(args, context) { if (!args[0]) { throw new coda.UserVisibleError("Parameter is required"); } let response = await context.fetcher.fetch({...}); if (response.status === 404) { throw new coda.UserVisibleError("Item not found"); } if (response.status !== 200) { throw new coda.UserVisibleError( `API error: ${response.status} - ${response.body.message}` ); } return response.body; } ``` ### Status Code Error ```typescript // Automatically throw error for non-2xx responses let response = await context.fetcher.fetch({ method: "GET", url: "https://api.example.com/endpoint", cacheTtlSecs: 0, // Disable caching throwOnError: true, // Throw on non-2xx status }); ``` ## Caching ### Cache Control ```typescript // Set cache TTL (time-to-live in seconds) let response = await context.fetcher.fetch({ method: "GET", url: "https://api.example.com/data", cacheTtlSecs: 300, // Cache for 5 minutes }); // Disable caching cacheTtlSecs: 0 ``` ### When to Cache - ✅ Reference data (countries, categories) - ✅ Slowly changing data (user profiles) - ❌ Real-time data (stock prices, live scores) - ❌ User-specific data that changes frequently ## Best Practices ### Formula Development 1. **Descriptive names**: Clear, concise, descriptive names 2. **Good descriptions**: Explain what the formula does and when to use it 3. **Parameter validation**: Check required parameters early 4. **Error messages**: Clear, actionable error messages 5. **Examples**: Add examples to aid documentation ```typescript pack.addFormula({ name: "ConvertCurrency", description: "Convert amount from one currency to another using current exchange rates", parameters: [ coda.makeParameter({ type: coda.ParameterType.Number, name: "amount", description: "The amount to convert", }), coda.makeParameter({ type: coda.ParameterType.String, name: "from", description: "Source currency code (e.g., USD, EUR)", }), coda.makeParameter({ type: coda.ParameterType.String, name: "to", description: "Target currency code (e.g., USD, EUR)", }), ], resultType: coda.ValueType.Number, examples: [ { params: [100, "USD", "EUR"], result: 85.50 }, { params: [50, "GBP", "USD"], result: 65.75 }, ], execute: async function([amount, from, to], context) { // Implementation }, }); ``` ### Sync Table Best Practices 1. **Identity**: Always set `identityName` and `idProperty` 2. **Display property**: Choose the most meaningful column 3. **Featured properties**: Show 3-5 most important columns by default 4. **Continuations**: Handle pagination for large datasets 5. **Parameters**: Add filter parameters to reduce API calls ### API Integration 1. **Rate limits**: Respect API rate limits 2. **Pagination**: Handle paginated responses properly 3. **Error handling**: Graceful handling of API errors 4. **Authentication**: Use appropriate auth type 5. **Caching**: Cache when appropriate to reduce API calls ### Performance 1. **Minimize API calls**: Batch requests when possible 2. **Use caching**: Cache stable data 3. **Optimize schemas**: Only include needed properties 4. **Continuations**: Use for large datasets 5. **Timeout awareness**: 60-second execution limit ## Common Patterns ### Handling API Pagination ```typescript execute: async function(args, context) { let page = context.sync.continuation?.page || 1; let allItems = context.sync.continuation?.items || []; let response = await context.fetcher.fetch({ method: "GET", url: `https://api.example.com/items?page=${page}`, }); allItems = allItems.concat(response.body.items); let continuation; if (response.body.has_more) { continuation = { page: page + 1, items: allItems, }; } return { result: allItems, continuation: continuation, }; } ``` ### Building URLs with Parameters ```typescript execute: async function([filter, sortBy], context) { let params = new URLSearchParams(); if (filter) params.append("filter", filter); if (sortBy) params.append("sort", sortBy); let url = `https://api.example.com/items?${params.toString()}`; let response = await context.fetcher.fetch({ method: "GET", url }); return response.body; } ``` ### Transforming API Data ```typescript execute: async function(args, context) { let response = await context.fetcher.fetch({ method: "GET", url: "https://api.example.com/users", }); // Transform API data to match schema let users = response.body.users.map(user => ({ id: user.user_id, // Map API field to schema name: user.full_name, email: user.email_address, isActive: user.status === "active", })); return { result: users }; } ``` ## Development Commands ```bash # Install dependencies npm install # Upload/deploy pack npx coda upload pack.ts --notes "Description of changes" # Validate pack npx coda validate pack.ts # Execute formula locally (for testing) npx coda execute pack.ts FormulaName param1 param2 # Create new pack npx coda init ``` ## Testing ### Unit Testing ```typescript import { executeFormulaFromPackDef } from '@codahq/packs-sdk/dist/development'; import { pack } from './pack'; // Test a formula let result = await executeFormulaFromPackDef( pack, "FormulaName", ["param1", "param2"] ); console.assert(result === expectedValue); ``` ### Common Issues **"Network domain not whitelisted"** - Add the domain with `pack.addNetworkDomain()` **"Formula already exists"** - Formula names must be unique within the pack **"Invalid parameter type"** - Check parameter types match expected values **"Execution timeout"** - Use continuations for long-running operations - Optimize API calls and data processing **"Authentication failed"** - Verify auth configuration - Check user credentials - Ensure proper headers/tokens ## Resources ### Official Documentation - **Official Docs**: https://coda.io/packs/build/latest/ - **SDK Reference**: https://coda.io/packs/build/latest/reference/sdk/ - **Pack Studio**: https://coda.io/packs/build - **Example Packs**: Available in Pack Studio ### Skill Reference Guides - **[Advanced Sync Tables](references/advanced-sync-tables.md)** - Two-way sync, dynamic tables, property options - **[Quick Examples](references/quick-examples.md)** - Common formula and sync table patterns - **[Troubleshooting](references/troubleshooting.md)** - Common errors and debugging strategies ## Quick Reference ### Common Imports ```typescript import * as coda from "@codahq/packs-sdk"; ``` ### Pack Initialization ```typescript export const pack = coda.newPack(); ``` ### Add Formula ```typescript pack.addFormula({ name, description, parameters, resultType, execute }); ``` ### Add Sync Table ```typescript pack.addSyncTable({ name, identityName, schema, formula }); ``` ### Make Parameter ```typescript coda.makeParameter({ type, name, description, optional }); ``` ### Make Schema ```typescript coda.makeObjectSchema({ properties, displayProperty, idProperty }); ``` ### Fetch Data ```typescript await context.fetcher.fetch({ method, url, headers, body }); ``` ### Throw Error ```typescript throw new coda.UserVisibleError("Error message"); ```