Source: https://cli.nylas.com/guides/email-to-coda

# Push Email into a Coda Doc (CLI)

Teams plan in Coda — sprint trackers, CRM tables, intake queues — and the source data lands in email first. The usual bridge is a paid connector that charges per row. The CLI hands you each message as JSON; the Coda API inserts a row per message into the table you choose. This guide builds an email-to-Coda pipeline you control, mapping subject, sender, and date to Coda columns and running it on a schedule for free.

Written by [Caleb Geene](https://cli.nylas.com/authors/caleb-geene) Director, Site Reliability Engineering

Reviewed by [Qasim Muhammad](https://cli.nylas.com/authors/qasim-muhammad)

Updated June 9, 2026

> **TL;DR:** Pull messages with `nylas email search --json`, then insert one Coda row per message by POSTing to the Coda API `/docs/{docId}/tables/{tableId}/rows` endpoint with a token. The CLI handles the inbox across six providers; the Coda API handles the write. One detail below trips up every first run — the cell key that Coda silently ignores if you get it wrong.

Command references used in this guide: [`nylas email search`](https://cli.nylas.com/docs/commands/email-search), [`nylas email list`](https://cli.nylas.com/docs/commands/email-list), and [`nylas email read`](https://cli.nylas.com/docs/commands/email-read).

## How do you push email into a Coda doc?

You push email into Coda by pulling each message as JSON and inserting one row per message into a Coda table. The CLI returns structured messages with `nylas email search --json`, and a single POST to the Coda API `/docs/{docId}/tables/{tableId}/rows` endpoint inserts rows with your mapped cells. The insert contract is documented in the [Coda API reference](https://coda.io/developers/apis/v1#operation/upsertRows).

Two setup steps come first. Generate an API token from your Coda account settings, then find the doc ID and table ID. Both IDs are visible in the doc URL, and the API token carries the same access as your account, so scope it to a service user when the table holds shared data. After that, each message maps to one row.

The `nylas email search` command returns messages as a JSON array with `--json`, including subject, sender, and timestamp. Scoping the query keeps the row count predictable. The example below pulls up to 50 matching messages from the last day into a file you process row by row.

```bash
# Pull the messages you want to file into Coda
nylas email search "subject:invoice" --after 2026-06-08 --json --limit 50 > items.json
```

## How do you map a message to Coda cells?

Map each message to a Coda `rows` array, where every row carries a `cells` list of column-and-value pairs. Each cell needs a `column` key set to a column ID or name and a `value`. Coda silently drops cells whose column key matches nothing, so a typo loses data with no error.

Build the payload with `jq` so the mapping is explicit and null-safe. A message with no subject would write a blank cell, so fall back to a placeholder. The Coda API accepts up to 1,000 rows in a single insert request, but a per-message loop keeps the code readable for low volumes.

```bash
DOC_ID="your-doc-id"
TABLE_ID="grid-your-table-id"
jq -c '.[]' items.json | while read -r msg; do
  subject=$(echo "$msg" | jq -r '.subject // "(no subject)"')
  sender=$(echo "$msg"  | jq -r '.from[0].email // ""')
  date=$(echo "$msg"    | jq -r '.date // ""')
  curl -s -X POST "https://coda.io/apis/v1/docs/$DOC_ID/tables/$TABLE_ID/rows" \
    -H "Authorization: Bearer $CODA_TOKEN" \
    -H "Content-Type: application/json" \
    -d "{\"rows\":[{\"cells\":[
          {\"column\":\"Subject\",\"value\":\"$subject\"},
          {\"column\":\"Sender\",\"value\":\"$sender\"},
          {\"column\":\"Received\",\"value\":\"$date\"}]}]}"
done
```

## Why do Coda rows duplicate, and how do you stop it?

Coda rows duplicate because a plain insert always creates a new row, even for a message you already filed. The fix is upsert: send a `keyColumns` array on the same rows endpoint, and Coda matches existing rows on those columns instead of appending. Use the message ID as the key so a re-run updates the same row.

The Nylas message ID is stable across syncs, which makes it the right upsert key. Store it in a dedicated column and add that column to `keyColumns`. Coda processes the upsert asynchronously and returns a 202 within about 200ms, so the row appears a moment after the request returns.

```bash
id=$(echo "$msg" | jq -r '.id')
curl -s -X POST "https://coda.io/apis/v1/docs/$DOC_ID/tables/$TABLE_ID/rows" \
  -H "Authorization: Bearer $CODA_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"keyColumns\":[\"MessageId\"],
       \"rows\":[{\"cells\":[
         {\"column\":\"MessageId\",\"value\":\"$id\"},
         {\"column\":\"Subject\",\"value\":\"$subject\"}]}]}"
```

## How do you keep the Coda table in sync?

Keep the Coda table in sync by running the pipeline on a schedule and upserting on the message ID. Scope the search to one day with `--after` and run it daily, so each pass only handles the last 24 hours of mail. A daily cron keeps the table current without re-importing the whole inbox, and upsert guarantees no duplicate rows.

For near-real-time intake, drive the same insert from a webhook instead of a poll. Register a `message.created` trigger with `nylas webhook create` and run the mapping step on each delivery. The Coda token is separate from the mailbox grant the tool manages, so rotate them on independent schedules.

```bash
# Real-time path: notify on every new message, then insert the row
nylas webhook create --url https://example.com/hooks/coda --triggers message.created
```

## What auth does the Coda pipeline need?

The Coda pipeline needs two independent credentials: a Coda API token for the write, and a Nylas grant for the mailbox read. The CLI stores the grant in your system keyring and refreshes its OAuth token automatically every 3,600 seconds, so the read side needs no manual renewal once you authenticate. The Coda token stays valid until you revoke it.

Install the tool with Homebrew, then authenticate once. See [getting started](https://cli.nylas.com/guides/getting-started) for the other install methods. The Coda API token goes in an environment variable, never in the command line, so it stays out of your shell history.

```bash
brew install nylas/nylas-cli/nylas
nylas auth login
export CODA_TOKEN="your-coda-api-token"
```

## Next steps

- [Automate Sales Follow-Up Emails (CLI)](https://cli.nylas.com/guides/sales-followup-email-cli) — Run a sales follow-up cadence from the terminal
- [Create Confluence Pages from Email](https://cli.nylas.com/guides/email-to-confluence-pages) — Pull email as JSON with the Nylas CLI and POST to the Confluence…
- [Send email to a Notion database](https://cli.nylas.com/guides/email-to-notion) — the same pattern into Notion pages
- [Email to Airtable](https://cli.nylas.com/guides/email-to-airtable) — batch records into an Airtable base
- [Email to Postgres](https://cli.nylas.com/guides/email-to-postgres) — store messages in a relational table
- [Extract email data with jq](https://cli.nylas.com/guides/extract-email-data-jq) — the JSON-shaping toolkit
- [Parse inbound email webhooks](https://cli.nylas.com/guides/parse-inbound-email-webhooks) — file messages in real time
- [Full command reference](https://cli.nylas.com/docs/commands) — every flag and subcommand documented
- [Coda API: insert/upsert rows](https://coda.io/developers/apis/v1#operation/upsertRows) — the official rows endpoint contract
- [jq manual](https://jqlang.github.io/jq/manual/) — filters for shaping the JSON payload
- [RFC 5322](https://datatracker.ietf.org/doc/html/rfc5322) — the internet message format behind the email fields
