DEV Community

GDPR retention and erasure for an agent mailbox

GDPR retention and erasure for an agent mailbox

Most "AI email" demos never think about deletion. The agent reads, replies, files things away, and the inbox just grows. That's fine in a demo. It is a problem the first time a real person emails your agent, because the moment that mailbox holds someone else's name, address, order history, or support complaint, you've taken on a data-protection obligation - and "we kept everything forever" is not a defensible retention policy.

An Agent Account on Nylas accumulates personal data you have to be able to purge. It's a mailbox the agent owns - support@yourcompany.com answering to a model instead of a human - and every inbound message lands in it. Under GDPR that data needs two things you can prove: a retention window so it doesn't live forever, and an erasure path so you can delete a specific person's mail when they ask. This post builds both, with the curl and the CLI for each step.

A quick, honest caveat before any of it: this is a docs-and-demo walkthrough, not legal advice. The Nylas primitives below cover the mail held in the mailbox. Any derived copy you made - rows in your own database, lines in your application logs, a vector store you embedded the message into - is yours to purge separately. The API can delete the message; it can't reach into your Postgres. Keep that in mind throughout.

What the platform gives you

Nothing new to learn on the data plane. An Agent Account is just a grant with a grant_id, so everything you already know about Messages and Threads applies directly - listing, reading, and deleting mail run against the same grant-scoped endpoints any other Nylas integration uses.

Retention and erasure split cleanly into two layers:

  • Retention is a control-plane setting. It lives on a policy - an application-scoped resource that bundles limits and spam settings - attached to the workspace your Agent Account belongs to. Two fields cap how long mail survives: limit_inbox_retention_period and limit_spam_retention_period. Set them once and Nylas deletes aged-out mail for you, per account, with no cron job on your side.

  • Erasure is a data-plane operation. To honor a right-to-erasure request for one person, you find their messages with GET /v3/grants/{grant_id}/messages filtered by sender, then hard-delete each one with DELETE .../messages/{id}?hard_delete=true - a plain delete only trashes the message, which isn't erasure. For a full identity wipe, you delete the grant itself.

The split matters because the two answer different questions. Retention answers "how long do we keep anything?" - a blanket time bound. Erasure answers "delete this person's data, now" - a targeted request. A compliant agent mailbox needs both; one doesn't substitute for the other.

I work on the Nylas CLI, so the terminal commands below are the exact ones I reach for. Where it matters I'll show the curl and the nylas ... form side by side, because in practice you set retention once through the API and run erasure ad hoc from a terminal when a request comes in.

Before you begin

You need an Agent Account, an API key for the same application, and the host set to https://api.us.nylas.com. Every API call below carries Authorization: Bearer <NYLAS_API_KEY>.

If you don't have an account yet, you provision one against a registered domain - either the API:

curl --request POST \
  --url "https://api.us.nylas.com/v3/connect/custom" \
  --header "Authorization: Bearer <NYLAS_API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{
    "provider": "nylas",
    "settings": {
      "email": "support@yourcompany.com"
    },
    "name": "Support Agent"
  }'

or the CLI, which creates the nylas connector for you if it doesn't exist yet:

nylas agent account create support@yourcompany.com --name "Support Agent"

Either path returns a grant_id. The API also auto-creates a default workspace and a default policy for the account - that default workspace is where you'll attach the retention policy in a moment.

Full background lives in the Agent Accounts docs and the Policies, Rules, and Lists guide. CLI reference is at cli.nylas.com/docs/commands.

One plan note up front: on the free plan, retention defaults to 30 days in the inbox and 7 days in spam, and those defaults aren't configurable. The two retention fields below are configurable on paid plans, where you set your own windows. So if you're prototyping on the free tier, you already have a 30-day inbox bound whether you ask for it or not - which is a reasonable default, but probably not the number your legal team picked.

Set retention windows on a policy

Retention is two fields inside a policy's limits object:

  • limit_inbox_retention_period - days a message stays in the inbox before Nylas deletes it.
  • limit_spam_retention_period - days a message stays in spam before deletion. This must be shorter than the inbox window, so spam clears out ahead of legitimate mail. If you set spam longer than inbox, the API rejects it.

Both take a number of days. Pick windows that match your documented retention schedule - the number you'd write down for an auditor, not a number you guessed.

Here's a policy that keeps inbox mail for a year and spam for 30 days:

curl --request POST \
  --url "https://api.us.nylas.com/v3/policies" \
  --header "Authorization: Bearer <NYLAS_API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{
    "name": "Support Agent Retention Policy",
    "limits": {
      "limit_inbox_retention_period": 365,
      "limit_spam_retention_period": 30
    }
  }'

The CLI takes the same JSON body through --data, so the policy you describe is identical:

nylas agent policy create --data '{
  "name": "Support Agent Retention Policy",
  "limits": {
    "limit_inbox_retention_period": 365,
    "limit_spam_retention_period": 30
  }
}'

Both return a policy_id. Hold onto it - a policy does nothing on its own. It only takes effect once a workspace points at it, and every Agent Account in that workspace inherits the windows.

You attach it to the default workspace your account already landed in:

curl --request PATCH \
  --url "https://api.us.nylas.com/v3/workspaces/<WORKSPACE_ID>" \
  --header "Authorization: Bearer <NYLAS_API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{
    "policy_id": "<POLICY_ID>"
  }'
nylas workspace update <WORKSPACE_ID> --policy-id <POLICY_ID>

That's the entire retention story on the platform side. From here, Nylas enforces the windows itself - there's no scheduled job for you to write, monitor, or page on when it silently stops running at 3am. The part I like as an SRE is exactly that: retention isn't a script that can rot. It's a property of the account, declared once, enforced by the platform.

A workspace with no policy attached runs its accounts at your plan's maximum limits - which on a paid plan can mean no retention bound at all. So "we have an Agent Account" does not imply "we have a retention window." Attaching the policy is the step that makes the claim true.

If you ever need to widen or tighten the window, PATCH the policy and every account in the workspace follows; on the default workspace, policy_id and rule_ids are the only fields you're allowed to change, which keeps this hard to get wrong.

Build the erasure path

Retention handles the passive case - mail aging out on a schedule. Erasure handles the active one: a named individual exercises their right to be forgotten and you have to remove their mail specifically, today, regardless of how old it is.

The shape is two steps. First, find every message from that person. Then delete each one. Both run against the grant, on the same Messages endpoints any Nylas app uses - there's nothing Agent-Account-specific here, which is the point.

Step 1 - find the subject's messages

Filter the message list by sender. The Messages API takes a from query parameter that matches the sender address, so a single call returns the candidate set:

curl --request GET \
  --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages?from=jane.doe@example.com&limit=50" \
  --header "Authorization: Bearer <NYLAS_API_KEY>"

The CLI exposes the same filter as --from, and --id prints the message IDs you'll feed into the delete step:

nylas email list <NYLAS_GRANT_ID> --from jane.doe@example.com --id

A few honest things to watch for, because "find their messages" is deceptively simple:

  • --from matches the sender, so this finds mail the subject sent to the agent. Mail the agent sent to the subject lives in the Sent folder with the agent as sender - pass --folder SENT (or list all folders) and filter on the recipient side if your erasure scope includes outbound copies.
  • One person can email from more than one address; if you only have a name, you may need to run the lookup for each known address. Don't assume a single from value captures everything tied to a person.

Step 2 - hard-delete each message

This is the step where the obvious call does the wrong thing, and it matters more here than anywhere else in the post. A plain DELETE on a message does not erase it - it moves the message to Trash. For a right-to-erasure request, trashed mail is still retained mail, and "we moved it to a folder" is not deletion.

To actually remove a message, you pass ?hard_delete=true, which deletes it immediately instead of trashing it, and that operation is irreversible:

curl --request DELETE \
  --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages/<MESSAGE_ID>?hard_delete=true" \
  --header "Authorization: Bearer <NYLAS_API_KEY>"

Two things gate that flag:

  1. hard_delete has to be enabled for your application - turn on Enable hard delete in the Nylas Dashboard under Customizations > API before the parameter does anything.
  2. The CLI cannot hard-delete. nylas email delete only moves a message to Trash - there is no hard_delete flag on it. So for true erasure of an individual message, the API is your only path:
# CLI: trashes the message - does NOT permanently erase it
nylas email delete <MESSAGE_ID> <NYLAS_GRANT_ID>

That's fine for an inbox-cleanup workflow, but don't reach for it to satisfy an erasure request and assume the data is gone - it's sitting in Trash.

Threads have the same trap, with no escape hatch. Deleting a thread (DELETE /v3/grants/{grant_id}/threads/{thread_id}, or nylas email threads delete) moves the thread and all its messages to Trash. There is no hard_delete parameter on the thread endpoint at all - so thread deletion is a convenience for clearing a conversation out of view, not an erasure primitive. If a single back-and-forth carried the personal data across several replies, list the thread's messages and hard-delete each one individually; don't rely on the thread delete to erase them.

So the erasure loop is: list by sender, collect the message IDs, then DELETE .../messages/{id}?hard_delete=true on each, through the API.

Keep an audit record of what you deleted and when - the message IDs and a timestamp - without storing the message content you just erased. A proof-of-erasure log that quietly retains the bodies defeats the entire exercise; log the fact of deletion, not the data.

Full erasure: delete the whole mailbox

Sometimes the subject is the agent identity. If the personal data tied to one person is the entire reason an Agent Account exists - a per-customer agent mailbox, say, that you spun up for a single client who's now offboarding - hard-deleting individual messages is the long way around, and it's also the one case where the CLI gives you a clean full wipe.

Deleting the grant removes the whole mailbox: every message, thread, folder, and the connection itself. This is the only full-erasure path that doesn't depend on the hard_delete toggle, because it tears down the mailbox rather than emptying it message by message.

curl --request DELETE \
  --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>" \
  --header "Authorization: Bearer <NYLAS_API_KEY>"

The CLI does this through the agent-account or auth commands - both revoke the same underlying grant:

nylas agent account delete support@yourcompany.com --yes
nylas auth revoke <NYLAS_GRANT_ID>

This is the nuclear option, and it's the right one for full erasure of an agent identity. It's also irreversible - the grant and its mailbox are gone, with no undelete.

Reach for per-message hard-delete when you're scrubbing one person out of a shared agent mailbox, and grant deletion when the entire mailbox represents the person you're erasing.

Guardrails and honest limits

A few things the demo glosses over that a compliance review won't:

  • Custom metadata isn't available on Agent Accounts. On a normal grant you might tag messages with a subject ID in metadata to make erasure lookups trivial. That's not an option here - Agent Accounts don't support custom metadata - so your erasure index has to live in your system, keyed off the message IDs you already store. Plan for that when you design the handler.

  • Derived copies are your problem, not the API's. This is worth repeating because it's the most common gap. If your agent summarized a thread into your database, embedded a message into a vector store, or wrote the sender address into a log line, deleting the message from the mailbox leaves all of those untouched. Erasure is only complete when every copy is gone. Build the deletion of your derived data into the same handler that calls the Nylas DELETE.

  • Retention defaults aren't a policy you chose. The free-plan 30-day inbox / 7-day spam window is a default, not a decision. If you're on a paid plan and never attached a policy, your accounts run at plan maximums - potentially no bound at all. Don't mistake "it seems to clean up" for "we have a documented retention schedule." Attach the policy explicitly.

  • limit_spam_retention_period must be shorter than limit_inbox_retention_period. It's a hard constraint the API enforces. Set both, and set spam lower.

  • A plain delete is not erasure. This is the single most important thing to get right in this post. A bare DELETE on a message moves it to Trash; thread deletion trashes the whole conversation. Only ?hard_delete=true on the message endpoint actually removes the data.

Comments

No comments yet. Start the discussion.