# n8n

> Give an n8n workflow its own email address with Pidgeon: a trigger that starts it when mail arrives, actions to reply and save drafts, and three templates to import.

Give an [n8n](https://n8n.io) workflow an email address of its own. Mail that
arrives starts the workflow; the workflow replies, saves drafts for a person
to send, hands threads to people and waits for verification codes. Nobody's
own inbox is connected to n8n, and a model in the workflow only ever reads the
one message in front of it.

**Use an address on `pidgeon.email` now, or connect your own domain.** Pidgeon
writes the SPF, DKIM and DMARC records for a domain you connect; the domain
stays yours, and we do not sell domains. The inboxes you already have
elsewhere stay where they are.

The package is `@pidgeon-ai/n8n-nodes-pidgeon`, a community node. It has two
nodes:

- **Pidgeon Trigger** starts a workflow when mail arrives at an address, or
  when a thread is handed to a person or back to the agent.
- **Pidgeon** acts: create and list addresses; get, search, send and reply to
  messages; wait for a verification code; save and send drafts; hand a thread
  to a person or take it back; download an attachment.

It was run end to end on n8n 2.44.6, against production, with a test key.

## Install

On a self-hosted n8n, as an owner or admin: **Settings → Community Nodes →
Install**, and enter the package name.

```text
@pidgeon-ai/n8n-nodes-pidgeon
```

n8n 3.0 stops loading community nodes it has not verified unless
`N8N_UNVERIFIED_PACKAGES_ENABLED=true` is set, and this one is not verified
yet. On n8n 2.x it loads as it is.

**n8n Cloud** runs verified community nodes only, so the package is not
available there until n8n verifies it. On Cloud today, use n8n's own MCP
Client node ([below](#on-n8n-cloud-today)).

A trigger needs n8n on a public HTTPS URL, because Pidgeon delivers webhooks
only to HTTPS. n8n uses `WEBHOOK_URL` as that address. On a laptop, a tunnel
such as `cloudflared tunnel --url http://localhost:5678` gives you one.

## The credential

Make a key at [Developers → API keys](https://pidgeon.ai/app/developers/keys?utm_source=n8n),
then in n8n: **Credentials → New → Pidgeon API**, and paste it. n8n tests it
against `GET /v1/me`.

**One address and one key per workflow, with the least access it needs.**
Whatever the key reaches, the workflow can reach, and so can any model you
give the workflow's output to.

| The workflow                      | Access |
| --------------------------------- | ------ |
| Only triggers, searches and reads | read   |
| Saves drafts, hands threads over  | draft  |
| Replies or sends                  | send   |
| Creates addresses                 | admin  |

A **test key** (`pgn_test_…`) works the same way, and nothing it sends leaves
Pidgeon ([test mode](https://pidgeon.ai/docs/test-mode.md)). Build the workflow on one, deliver mail
to it with `POST /v1/test/inbound`, and switch the credential to a live key
when it works. A test key cannot create addresses.

## The trigger

Choose **Message Received** or **Thread Handed Off**, and the address to
watch. One address per trigger.

When the workflow starts listening, the trigger creates its own webhook on
Pidgeon for that address, keeps the signing secret where only n8n can read
it, and checks the signature on every delivery: a request that is not signed
by Pidgeon, or is more than five minutes old, gets `401` and starts nothing.
When the workflow stops, the webhook is deleted. Listening in the editor and
the published workflow each have their own webhook, and each counts against
your plan's [webhook endpoints](https://pidgeon.ai/docs/limits.md) while it exists.

### Message Received

Pidgeon's webhooks carry ids and metadata, never a body. With **Fetch the
message** on, the default, the trigger reads the message and its thread before
the workflow starts, so the item carries the text, the sender's
authentication, codes and links. Every field a router branches on is a plain
field, so IF and Switch nodes test it with no model in between:

```json
{
"event": "message.received",
"event_id": "0044ed22-…",
"message_id": "1a70b25d-…",
"thread_id": "463ec73a-…",
"address": "quotes@acme.pidgeon.email",
"from": "priya@supplier.de",
"from_name": "Priya Raman",
"to": ["quotes@acme.pidgeon.email"],
"cc": [],
"delivered_to": "quotes@acme.pidgeon.email",
"matched_by": "identity",
"subject": "Angebot 4471",
"received_at": "2026-10-25T09:14:03Z",
"spam": false,
"from_self": false,
"bcc_to_this_address": false,
"is_reply": false,
"firewall_verdict": "clean",
"firewall_signals": [],
"auth_spf": "pass",
"auth_dkim": "pass",
"auth_dmarc": "pass",
"thread_status": "open",
"thread_owner_type": "agent",
"thread_assignee_id": null,
"text": "Guten Tag, anbei unser Angebot …",
"reply_text": "Guten Tag, anbei unser Angebot …",
"codes": [],
"links": [],
"attachments": [
  { "id": "0b1d4e8c-…", "filename": "angebot.pdf", "content_type": "application/pdf", "size": 48211 }
],
"test": false
}
```

- **`text` is clean by default.** [Pidgeon Firewall](https://pidgeon.ai/docs/firewall.md) takes
  out anything hidden or addressed to an AI. `firewall_verdict` is
  `suspicious` when it found something: route that mail to a person, not to a
  model. **Original** is the text exactly as sent, one dropdown away.
- **`from_self` is the one to check before replying.** Mail one of your
  addresses sends to another arrives as ordinary mail.
- **`auth_dmarc` is `pass` only when the From is not forged.** Check it
  before acting on who an email says it is from.
- **`bcc_to_this_address`** is true when the address is in neither `to` nor
  `cc`. A BCC gives the agent one message, not a conversation: the reply goes
  to you, not to it.
- **`thread_owner_type`** is `human` once a person has taken the thread.
  Leave those alone.

Without **Fetch the message** the item has only what the webhook carried:
the ids, addresses, subject, Firewall verdict and attachments.

### Thread Handed Off

Starts when a person takes a thread or it is assigned to somebody (**To a
Person**), when it is handed back (**Back to the Agent**), or either. The
item:

```json
{
"event": "thread.updated",
"thread_id": "463ec73a-…",
"owner_type": "human",
"previous_owner_type": "agent",
"status": "open",
"assignee_id": null,
"assigned_by": null,
"reason": "Complaint about an order",
"address": "hello@acme.pidgeon.email",
"subject": "Order 1182 arrived broken",
"from": "dana@example.com",
"changed_at": "2026-10-25T10:02:11Z"
}
```

Use it to post to Slack or a CRM when a person must act, or to stop a
workflow that would answer a thread somebody now holds.

## The actions

| Operation                 | What it does                                                      | Access |
| ------------------------- | ----------------------------------------------------------------- | ------ |
| Address → Create          | A new address: agent or workflow, and who it may write to         | admin  |
| Address → Get Many        | The addresses the key reaches                                     | read   |
| Message → Get             | One message as the trigger's item, clean by default               | read   |
| Message → Search          | Messages by words, sender or folder                               | read   |
| Message → Wait for a Code | Holds until mail with a verification code arrives; returns `code` | read   |
| Message → Reply           | Writes to people outside your account: answers in the thread      | send   |
| Message → Send            | Writes to people outside your account: a new conversation         | send   |
| Draft → Save              | A draft in the address's Drafts folder, threaded as a reply       | draft  |
| Draft → Send              | Writes to people outside your account: sends a saved draft        | send   |
| Thread → Hand to a Person | Marks it as a person's, optionally assigned to somebody           | draft  |
| Thread → Take Back        | Gives it back to the agent                                        | draft  |
| Attachment → Download     | The file, as n8n binary data                                      | read   |

- **Sends are safe to retry.** Every write carries an `Idempotency-Key` made
  from your n8n, the workflow, the execution, the node and the item, so n8n's
  **Retry On Fail** replays a send whose answer was lost rather than sending
  it twice, and two n8n instances on one account never share a key.
- **Wait for a Code** does not fail when nothing arrives: the item has
  `timed_out: true`, for an IF node to branch on. It looks back two minutes
  first, so a code that arrived before the wait is not missed; pass the
  returned `cursor` as **After** to wait for the next one.
- **As an AI Agent tool**, the node offers the operation you chose when you
  added it, and the model fills in its fields. Give an agent Search or Save
  a draft, and keep sending as a step you wire.

**A draft waits because nothing sends it.** Pidgeon holds no draft back by
itself yet, and a `send` key that saves drafts can also send. To approve
before anything goes out, give the workflow a `draft` key, and send from the
web inbox, from a [mail client](https://pidgeon.ai/docs/mail-clients.md) (Drafts is a folder there
too), or with Slack's **Send and Wait for Response** followed by **Draft →
Send** on a `send` key.

## Templates

Each one is a workflow to import: in n8n, **Workflows → Import from URL**,
and paste the link. Each opens with a note that says how to set it up.

### Draft replies to emails you forward

[`forward-to-your-agent.json`](https://raw.githubusercontent.com/pidgeon-ai/pidgeon-examples/main/n8n/forward-to-your-agent.json)

You forward one email to the agent's address. The workflow checks the
forward came from one of your own addresses, passed DMARC and is clean; a
model drafts an answer in your style; and the draft comes back to you as a
reply, threaded under what you sent. You edit it and send it from your own
address. The address only answers people who wrote first, and the key is
`send` on that one address.

The trigger reads the **original** text here. Pidgeon treats a forwarded
original as quoted history, so the clean text of a forward holds only your
note on top. The Firewall still screens the whole message, which is why the
workflow goes on only when the verdict is `clean`.

Forward is for "answer this". BCC is for "watch what I send": the agent gets
your message, and the reply goes to you.

### Sort, draft and hand off inbound email

[`inbound-qualifier.json`](https://raw.githubusercontent.com/pidgeon-ai/pidgeon-examples/main/n8n/inbound-qualifier.json)

The public address (`hello@`, `sales@`) is the agent's. Spam, your own mail
and threads a person has taken are left alone. Rules on the subject and text
sort the rest, with no model: quote requests and orders get a drafted reply
saved for a person to send; complaints, anything the Firewall marks
suspicious and what the rules cannot place are handed to a person, and Slack
hears about it; out-of-office replies are dropped. The key is `draft` on that
one address.

### An address per workflow

[`address-per-workflow.json`](https://raw.githubusercontent.com/pidgeon-ai/pidgeon-examples/main/n8n/address-per-workflow.json)

A supplier that can only email its exports. The workflow owns an address,
checks the mail is from the expected sender and passed DMARC, downloads the
CSV attachment, and appends its rows to Google Sheets. The address must keep
mail: one without an inbox stores nothing, so there would be no file to
download. The key is `read` on that one address.

Pidgeon finds verification codes and links in mail. Parsing a file or
pulling names, dates and amounts out of text is n8n's job, or a model's.

All three are also in [pidgeon-ai/pidgeon-examples](https://github.com/pidgeon-ai/pidgeon-examples/tree/main/n8n).

## On n8n Cloud today

n8n's own **MCP Client** node can call Pidgeon's [hosted MCP server](https://pidgeon.ai/docs/mcp.md)
on Cloud now, with no package. Add an **MCP Client** node (or **MCP Client
Tool** under an AI Agent), set:

- **Server Transport:** HTTP Streamable
- **MCP Endpoint URL:** `https://pidgeon.ai/api/mcp`
- **Authentication:** Bearer Auth, with a Pidgeon key

and pick a tool, such as `list_addresses` or `await_message`. It has no
trigger: for mail as it arrives, a Webhook node needs a webhook you create
with `POST /v1/webhooks` and a Code node that checks
[the signature](https://pidgeon.ai/docs/webhooks/verifying.md).

## Limits

- The trigger watches one address. For several, add a trigger for each, with
  a key for each.
- Each listening trigger uses one webhook endpoint against your plan.
- Search returns at most 100 messages per run.
- Wait for a Code waits at most 280 seconds per run.
