codapack-dev/SKILL.md

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");
```