DEV Community

Add custom metadata to Nylas objects for filtering

What metadata is and where it lives

Metadata is a field of your own key-value pairs that you attach to a Nylas object, and it's supported on messages, drafts, events, and calendars. The values are strings, you can store up to 50 pairs on a single object, and each value can hold up to 500 characters. That's room for a campaign slug, an external record ID, a workflow status, and whatever else ties the object back to your application, all carried on the object itself rather than in a side table.

The point of metadata is to avoid that side table. Instead of mapping a Nylas message ID to a row in your database and joining on every read, you put your application's data on the message and read it straight back. The object becomes self-describing from your app's point of view, so the campaign a message belongs to or the order an event is for travels with the object through every API call that returns it. For application data that's small and that you want to filter by, this is simpler than maintaining a parallel store.

The five-key rule that decides your schema

This is the single most important thing to understand about metadata, and missing it leads to a tagging scheme that can't be queried. You can store up to 50 key-value pairs, but only five keys, literally named key1 through key5, are indexed and filterable. Any other key you set is stored and returned with the object, but you cannot filter on it. So the keys you plan to query by have to live in key1 through key5.

This shapes how you design your tags. The values you'll search for - the campaign ID, the workflow status, the tenant - go in the five indexed keys, and you decide that mapping up front: key1 is the campaign, key2 is the run, and so on. Everything else, data you want to carry but never filter by, can use descriptive key names and ride along as the other 45 pairs. Get this backwards, putting a filterable concept in a free-form key, and you'll have stored the data but built no way to query it, which usually surfaces later when the filter you need silently returns nothing.

Set metadata when you send

You attach metadata at creation time by adding a metadata object to the request. On a POST /messages/send, the metadata object carries your key-value pairs alongside the rest of the message, and they're stored on the resulting message for later retrieval and filtering.

curl --request POST \
  --url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/messages/send" \
  --header "Authorization: Bearer <NYLAS_API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{
    "to": [{ "email": "lead@example.com" }],
    "subject": "Your trial is ending",
    "body": "<p>A quick nudge.</p>",
    "metadata": {
      "key1": "q2-trial-nudge",
      "key2": "run-1183"
    }
  }'

Here key1 holds the campaign and key2 the run, both indexed so you can filter on either later.

The CLI sets the same metadata with a repeatable --metadata flag on nylas email send, written as key=value:

nylas email send --to lead@example.com --metadata key1=q2-trial-nudge --metadata key2=run-1183

Tag every send in a campaign, not just some, because an untagged message is invisible to the filter that counts the campaign later.

Filter objects by metadata

Reading tagged objects back uses the metadata_pair query parameter, written as metadata_pair=key1:value, which returns only objects whose metadata matches that key and value. On the messages endpoint, GET /v3/grants/{grant_id}/messages?metadata_pair=key1:q2-trial-nudge returns exactly the messages tagged for that campaign, no scan of every message required.

curl --request GET \
  --url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/messages?metadata_pair=key1:q2-trial-nudge" \
  --header "Authorization: Bearer <NYLAS_API_KEY>"

The same metadata_pair filter works on the drafts, events, and calendars list endpoints, so the pattern is identical whether you're pulling a campaign's messages or a project's events.

From the CLI:

nylas email list --metadata key1:q2-trial-nudge

This does the same filter, and its help is explicit that only key1 through key5 work here, which is the rule from earlier showing up at query time. This is the payoff of tagging: one filtered request replaces fetching everything and matching in your own code.

Update tags after creation, through the API

Setting metadata at creation doesn't lock you in. The message update endpoint, PUT /v3/grants/{grant_id}/messages/{message_id}, accepts a metadata object alongside unread, starred, and folders, so you can change a message's tags after it's sent, through the same partial-PUT call that marks it read or moves it to a folder. Include only the fields you want to change and the rest stay as they were, so re-tagging a sent message is a one-field update rather than a resend.

Events update the same way. Because you change an event with a PUT, its metadata goes along, so you can flip a key1 workflow status from pending to confirmed as the event progresses, which makes metadata useful for tracking state over time rather than just a fixed label.

The one place this differs is the CLI: nylas email send --metadata sets tags at send time and the nylas email metadata commands read them back, but the terminal doesn't expose a metadata-update flow, so to change a message's tags after sending you go through the API's message PUT. From the CLI, plan the tags before sending; from code, evolve them whenever you need.

Provider and reserved-key caveats

Two specifics are worth knowing before you lean on metadata heavily.

First, metadata_pair filtering isn't universal across providers: on CalDAV-backed calendars, which is how iCloud connects, the filter isn't supported for events, so a query that works against Google and Microsoft events won't behave the same there. If you support iCloud calendars, don't build a feature that depends on filtering their events by metadata, and fall back to listing and matching in your own code for those.

Second, key5 carries a special meaning in one place. Nylas uses key5 to identify the events that count toward the max-fairness round-robin calculation in group availability, so if you run round-robin scheduling, key5 is effectively spoken for on those events, where it holds the group identifier. It's still an ordinary indexed key everywhere else, on messages and on non-group events, but on group-availability events, leave key5 for the value scheduling expects rather than overloading it with your own tag.

Inspect what's tagged from the CLI

When you want to see what metadata is actually on an object, the CLI reads it back. nylas email metadata show <message-id> prints every metadata pair on a message, both the indexed key1 through key5 and any custom-named keys you added, so you can confirm a tag landed the way you intended. There's also nylas email metadata info for a quick reference on how the keys and filtering work.

# See every metadata pair on a message
nylas email metadata show <message-id>

This is the fast way to debug a tagging scheme: send a tagged message, run metadata show, and check that the campaign landed in key1 rather than a free-form key that won't filter. It's saved me more than once from the silent-empty-filter problem, where a tag was stored under the wrong key name and the metadata_pair query came back empty because the value wasn't in one of the indexed five.

Where metadata earns its place

The same tag-and-filter pattern sits under a range of features, and the value is always avoiding a side table. A few that map straight on:

  • Campaign attribution. Tag every send with a campaign ID in key1, then filter with metadata_pair to pull a campaign's messages for reply-rate or open-rate analysis.
  • Multi-tenant scoping. Stamp each object with a tenant ID so one connected account can serve many tenants and you filter each tenant's data cleanly.
  • Workflow status on events. Track an event through states by updating a key1 status, filtering for everything pending or confirmed without a separate status store.
  • Linking to your own records. Carry your database's primary key for the related record on the object, so a webhook or list result points straight back to your row with no lookup table.

Each replaces a join or a parallel store with a tag that travels on the object and a one-line filter that reads it back.

Things to keep in mind

A short list of details keeps metadata predictable:

  • Only key1 through key5 filter. You can store 50 pairs, but anything you'll query by must live in one of the five indexed keys; other keys are stored but not filterable.
  • Fifty pairs, 500 characters each. Values are strings capped at 500 characters, so store IDs and slugs, not document bodies.
  • Tags update through PUT. Both message metadata (via PUT /v3/grants/{id}/messages/{id}) and event metadata (via the event PUT) can change after creation; the partial PUT touches only the fields you send.
  • The CLI sets at send, doesn't update. nylas email send --metadata tags at send time with no terminal update flow, so compute the full tag set before sending when you work from the CLI, or use the API PUT to evolve them.
  • Filter with metadata_pair=key:value. The same parameter works on messages, drafts, events, and calendars.
  • Tag everything in a cohort. An untagged object is invisible to the filter, so tag every send or event in a group, not just some.

Wrapping up

Metadata lets you carry your own application data on a Nylas object and filter by it, instead of maintaining a side table and joining on every read. Attach up to 50 key-value pairs with a metadata object at send or create time, put anything you'll query by in the indexed key1 through key5, and read it back with metadata_pair=key1:value on the messages, drafts, events, or calendars endpoint. From the terminal, nylas email send --metadata tags and nylas email metadata show inspects.

Remember that both message and event tags can be updated through their PUT endpoints, even though the CLI only sets them at send, and that only the five indexed keys are filterable - the rest are along for the ride.

Where to go next

AI-answer pages for agents

When this post is published, link AI agents and crawlers to the retrieval-ready version on cli.nylas.com:

Comments

No comments yet. Start the discussion.