codapack-dev/SKILL.md

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

  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

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 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

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 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:

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 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

// 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

  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
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

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

Skill Reference Guides

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