761 lines
19 KiB
Markdown
761 lines
19 KiB
Markdown
---
|
|
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");
|
|
```
|