19 KiB
| name | description |
|---|---|
| coda-pack-dev | 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
- Formulas - Custom functions for calculations or data retrieval
- Actions - Special formulas that modify external applications (used in buttons/automations)
- Column Formats - Control how values display in tables
- Sync Tables - Automated tables that pull data from external sources
Pack Structure
Basic pack.ts Template
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
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 valuesParameterType.Number- Numeric valuesParameterType.Boolean- True/falseParameterType.Date- Date valuesParameterType.StringArray- Array of stringsParameterType.NumberArray- Array of numbersParameterType.Html- HTML contentParameterType.Image- Image URL or data
Result Types
Common result types:
ValueType.String- TextValueType.Number- NumbersValueType.Boolean- True/falseValueType.Array- Array of valuesValueType.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
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
// 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
fromKeyto map API field names to schema properties - Set
required: truefor mandatory fields - Use
displayPropertyfor the primary column - Use
idPropertyfor unique identification
Row Limits:
- Default: 1,000 rows
- Maximum: 10,000 rows
Continuations: For paginated results exceeding 60s timeout:
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:
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 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:
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 for:
- Property options (static and dynamic)
- Batch updates with
maxUpdateBatchSize - Error handling in updates
- OAuth incremental authorization
Authentication
Setting Up Authentication
// 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
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:
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
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 linksValueHintType.Email- Email addressesValueHintType.Date- Date formattingValueHintType.DateTime- Date and timeValueHintType.ImageReference- Display imagesValueHintType.ImageAttachment- Inline imagesValueHintType.Markdown- Render markdownValueHintType.Html- Render HTML
Array Schemas
// 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:
pack.addNetworkDomain("api.example.com");
pack.addNetworkDomain("cdn.example.com");
// Support subdomains
pack.addNetworkDomain("example.com"); // Allows *.example.com
Error Handling
User-Visible Errors
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
// 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
// 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
- Descriptive names: Clear, concise, descriptive names
- Good descriptions: Explain what the formula does and when to use it
- Parameter validation: Check required parameters early
- Error messages: Clear, actionable error messages
- Examples: Add examples to aid documentation
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
- Identity: Always set
identityNameandidProperty - Display property: Choose the most meaningful column
- Featured properties: Show 3-5 most important columns by default
- Continuations: Handle pagination for large datasets
- Parameters: Add filter parameters to reduce API calls
API Integration
- Rate limits: Respect API rate limits
- Pagination: Handle paginated responses properly
- Error handling: Graceful handling of API errors
- Authentication: Use appropriate auth type
- Caching: Cache when appropriate to reduce API calls
Performance
- Minimize API calls: Batch requests when possible
- Use caching: Cache stable data
- Optimize schemas: Only include needed properties
- Continuations: Use for large datasets
- Timeout awareness: 60-second execution limit
Common Patterns
Handling API Pagination
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
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
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
# 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
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 - Two-way sync, dynamic tables, property options
- Quick Examples - Common formula and sync table patterns
- Troubleshooting - Common errors and debugging strategies
Quick Reference
Common Imports
import * as coda from "@codahq/packs-sdk";
Pack Initialization
export const pack = coda.newPack();
Add Formula
pack.addFormula({ name, description, parameters, resultType, execute });
Add Sync Table
pack.addSyncTable({ name, identityName, schema, formula });
Make Parameter
coda.makeParameter({ type, name, description, optional });
Make Schema
coda.makeObjectSchema({ properties, displayProperty, idProperty });
Fetch Data
await context.fetcher.fetch({ method, url, headers, body });
Throw Error
throw new coda.UserVisibleError("Error message");