Skip to content

Design your Pack

Coda Packs don't include traditional user interface elements like dialogs or sidebars. Instead users interact with your building blocks using standard Coda interfaces, like the formula editor. However there are still many subtle design choices to make when building your Pack, and they can have a real impact on usability.

This page aims to provide the guidance you need to create a Pack that meets the needs and expectations of Coda users.

General guidance

No matter what kind of Pack you are building, there are some basic rules to keep in mind.

Build building blocks

Unlike other types of integrations, a Coda Pack doesn’t prescribe an exact end-to-end experience. Instead it provides a new set of building blocks, like formulas or buttons, that a user can deploy to improve their docs. These building blocks need to provide sufficient flexibility so that they can be combined in novel and bespoke ways.

  • Prefer parameters over hard-coding specific patterns.
  • Return structured data, so users can chain formulas together.

Don't

TasksDueWithin7Days() =>

<ul>
  <li>Send out TPS report - Monday</li>
  <li>Complete training - Wednesday</li>
  <li>Organize team lunch - Friday</li>
</ul>

Do

Tasks(dueWithin: Duration(7)) =>

[
  {
    description: "Send out TPS report",
    due: "2023-02-20",
  }
  // etc...
]

While building blocks offer great flexibility, you'll still want to show users how to apply them. When you publish, make sure to create a featured doc that includes some key use cases and demonstrates how your Pack can address them.

Build for users

The target user for your Pack is akin to a skilled spreadsheet user: they know how to use formulas and think about data, but they likely aren’t developers. When designing your Pack make sure it’s approachable to someone with this level of technical fluency.

When building a Pack that integrates with another application, the simplest approach is to create a thin wrapper on their API. However this may introduce terms and patterns not familiar to non-developers. Instead think of the Pack as an extension of the user experience, but translated from pixels to formulas.

  • Avoid using technical jargon when naming build blocks, parameters, or outputs.
  • Hide implementation details from the user, like API versions.
  • Don’t require users to understand technical formats like JSON, XML, etc.

Don't

InsertNewProjectRecord("v5",
  "{\"name\": \"My project\"}")

Do

CreateProject("My project")

Less is more

Developers love to have expansive APIs that provide complete access to all features, but too much choice can be overwhelming for a Pack user. When designing a Pack, focus on the 20% of functionality that will meet the needs of 80% of your users. Omit more advanced options or features at first, addressing them if/when there is sufficient demand.

  • Omit obscure advanced options, preferring instead sensible defaults that work well in the majority of cases.
  • Put the most important parameters first, and use optional parameters when a value is not strictly required.

Don't

AddTask(project, task, labels, reccurence,
  workflow, dueDate)

Do

AddTask(task, project, [dueDate])

Use simple names

When building a Pack you don’t need to worry about name collisions, and accessibility to users is more important than completeness or accuracy. When choosing a name, prefer simple nouns or verbs and remove any extraneous detail.

  • Don’t include the Pack or company name.
  • Avoid unnecessary detail in names, unless required to distinguish them.
  • Use names that sound more like ordinary speech.
  • Prefer single nouns or verbs when feasible.

Don't

AcmeTasksListAllTasks()
AcmeTasksCreateFromScannedImageUpload()
AcmeTasksSetAssignee()

Do

Tasks()
AddFromPhoto()
Reassign()

You can find more best practices for naming building blocks in the guides for formulas, actions, and sync tables.

API Integration

A common use case for Packs is integrating with another application or service using their API. While each integration is unique, there are certain patterns and conventions that can be useful to understand. This section includes some tips for designing a Pack around an existing API.

Select collections

Most REST APIs are organized into collections, usually corresponding specific types of items in the application. An API can contain dozens of collections, but as per the general guidance above it's best to start with the handful of core ones that are most valuable to users.

Example: Todoist

The Todoist API includes collections for Projects, Sections, Tasks, Comments, and Labels. While a power user may want to leverage all of that information, for most users Projects and Tasks are the core entities they'll want to work with.

Design the schema

Examine the data returned for each item in the collection and determine what to expose to users in Coda. Select the fields most important to users and start there. You can always add more fields later without breaking anything.

When designing your schema, select user-friendly names for your properties. The field in the API may use technical terminology or refer to an older name no longer in use by the product.

Example: Todoist task schema

The Todoist API returns up to 20 fields for a task, but for most use cases only a few are required. Additionally the name "content" is replaced with "name".

{
  "creator_id": "2671355",
  "created_at": "2019-12-11T22:36:50.000000Z",
  "assignee_id": "2671362",
  "assigner_id": "2671355",
  "comment_count": 10,
  "is_completed": false,
  "content": "Buy Milk",
  "description": "",
  "due": {
    "date": "2016-09-01",
    "is_recurring": false,
    "datetime": "2016-09-01T12:00:00.000000Z",
    "string": "tomorrow at 12",
    "timezone": "Europe/Moscow"
  },
  "id": "2995104339",
  "labels": ["Food", "Shopping"],
  "order": 1,
  "priority": 1,
  "project_id": "2203306141",
  "section_id": "7025",
  "parent_id": "2995104589",
  "url": "https://todoist.com/showTask?id=2995104339"
}
const TaskSchema = coda.makeObjectSchema({
  properties: {
    name: {
      description: "The name of the task.",
      type: coda.ValueType.String,
      fromKey: "content",
    },
    description: {
      description: "A description of the task.",
      type: coda.ValueType.String,
    },
    url: {
      description: "A link to the task.",
      type: coda.ValueType.String,
      codaType: coda.ValueHintType.Url,
    },
    id: {
      description: "The ID of the task.",
      type: coda.ValueType.String,
    },
  },
  displayProperty: "name",
  idProperty: "id",
  featuredProperties: ["description", "url"],
});

Add building blocks

For each collection, add a set of building blocks that allow users to work with them. The exact set of building blocks may vary from collection to collection, and use the guidance below as a starting point.

Requirements

  • The API has an endpoint for retrieving all the items in the collection (ex: GET /tasks).

A sync table exposes a collection as a special Coda table, allowing users to work with large sets of data using familiar conventions.

  • If the API endpoint support filtering the results, consider exposing those as parameters on the sync table to allow for faster, more targeted syncs.
  • If the API paginates the results, use continuations to spread the requests over multiple executions and avoid timeouts.
Example: Todoist Tasks sync table
GET https://api.todoist.com/rest/v2/tasks?
    project_id=<project ID>&
    section_id=<section ID>&
    label=<label name>&
    filter=<filter string>&
    lang=<language code>&
    ids=<list of IDs>
pack.addSyncTable({
  name: "Tasks",
  schema: TaskSchema,
  identityName: "Task",
  formula: {
    name: "SyncTasks",
    description: "Sync tasks",
    parameters: [
      coda.makeParameter({
        type: coda.ParameterType.String,
        name: "filter",
        description: "A supported filter string. See the Todoist help center.",
        optional: true,
      }),
      coda.makeParameter({
        type: coda.ParameterType.String,
        name: "project",
        description: "Limit tasks to a specific project.",
        optional: true,
        autocomplete: async function (context, search) {
          let url = "https://api.todoist.com/rest/v2/projects";
          let response = await context.fetcher.fetch({
            method: "GET",
            url: url,
          });
          let projects = response.body;
          return coda.autocompleteSearchObjects(search, projects, "name", "id");
        },
      }),
    ],
    execute: async function ([filter, project], context) {
      let url = coda.withQueryParams("https://api.todoist.com/rest/v2/tasks", {
        filter: filter,
        project_id: project,
      });
      let response = await context.fetcher.fetch({
        method: "GET",
        url: url,
      });

      let results = [];
      for (let task of response.body) {
        results.push({
          name: task.content,
          description: task.description,
          url: task.url,
          id: task.id,
        });
      }
      return {
        result: results,
      };
    },
  },
});

Requirements

  • The API has an endpoint for retrieving a specific item by ID (ex: GET /tasks/123).
  • The ID of an item is user-visible (or can be obtained from a user-visible URL).

A "getter" formula allows users to retrieve the details of a specific item, which can then be composed with other formulas or tables.

  • The formula should take the ID and/or URL as a parameter, and return an object matching the defined schema.
Example: Todoist Task() formula
GET https://api.todoist.com/rest/v2/tasks/<taskId>
pack.addSyncTable({
  name: "Tasks",
  schema: TaskSchema,
  identityName: "Task",
  formula: {
    name: "SyncTasks",
    description: "Sync tasks",
    parameters: [
      coda.makeParameter({
        type: coda.ParameterType.String,
        name: "filter",
        description: "A supported filter string. See the Todoist help center.",
        optional: true,
      }),
      coda.makeParameter({
        type: coda.ParameterType.String,
        name: "project",
        description: "Limit tasks to a specific project.",
        optional: true,
        autocomplete: async function (context, search) {
          let url = "https://api.todoist.com/rest/v2/projects";
          let response = await context.fetcher.fetch({
            method: "GET",
            url: url,
          });
          let projects = response.body;
          return coda.autocompleteSearchObjects(search, projects, "name", "id");
        },
      }),
    ],
    execute: async function ([filter, project], context) {
      let url = coda.withQueryParams("https://api.todoist.com/rest/v2/tasks", {
        filter: filter,
        project_id: project,
      });
      let response = await context.fetcher.fetch({
        method: "GET",
        url: url,
      });

      let results = [];
      for (let task of response.body) {
        results.push({
          name: task.content,
          description: task.description,
          url: task.url,
          id: task.id,
        });
      }
      return {
        result: results,
      };
    },
  },
});

Requirements

  • The Pack has a "getter" formula (previous section) that only requires a single parameter.

A column format makes it easier for users to work with items in tables, enriching simple values with rich data.

  • If the formula accepts a URL, add a matcher for the URL pattern so the column format can be automatically applied.
Example: Todoist Task column format
pack.addColumnFormat({
  name: "Task",
  formulaName: "Task",
  // If the first values entered into a new column match these patterns then
  // this column format will be automatically applied.
  matchers: TaskUrlPatterns,
});

Requirements

  • The API has a endpoints for manipulating the collection, for instance:
    • Creating an item (ex: POST /tasks)
    • Updating an item (ex: PUT /tasks/123)
    • Deleting an item (ex: DELETE /tasks/123)
    • Performing a custom action (ex: POST /tasks/123:notify)

An action formula allows users to update the items from within their Coda doc, either in buttons or automations. Any API calls that have side effects (change the state of the app being integrated with) should be exposed as action formulas, as regular formulas can be executed at any time by the formula engine.

  • When creating or updating items, use optional parameters to capture the values for individual fields.
  • In addition to a generic update action, consider adding streamlined action formulas for common tasks (ex: Reassign, ChangeAddress, etc.).
Example: Todoist AddTask() action formula
POST https://api.todoist.com/rest/v2/tasks
{
  "content": "Buy milk"
}
pack.addFormula({
  name: "AddTask",
  description: "Add a new task.",
  parameters: [
    coda.makeParameter({
      type: coda.ParameterType.String,
      name: "name",
      description: "The name of the task.",
    }),
  ],
  resultType: coda.ValueType.String,
  isAction: true,

  execute: async function ([name], context) {
    let response = await context.fetcher.fetch({
      url: "https://api.todoist.com/rest/v2/tasks",
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        content: name,
      }),
    });
    // Return values are optional but recommended. Returning a URL or other
    // unique identifier is recommended when creating a new entity.
    return response.body.url;
  },
});