# Dify

> Give a Dify agent or workflow its own email address: tools to send, reply, search, wait for a verification code and hand a thread to a person.

Give a [Dify](https://dify.ai) agent or workflow an email address of its
own. It sends, replies in the thread, searches its mail, waits for a
verification code and hands a thread to a person. Nobody's own inbox is
connected to Dify.

The plugin is **Pidgeon**, a tool plugin built on Pidgeon's
[Python client](https://pidgeon.ai/docs/sdks.md). It has six tools:

| Tool           | What it does                                                                                       |
| -------------- | -------------------------------------------------------------------------------------------------- |
| Create Address | Creates an address. It can only answer people who wrote first unless you choose Anybody            |
| Send Email     | Starts a new conversation from one of your addresses. Sends real mail                              |
| Reply to Email | Answers a message by `message_id`, in its thread, from the address it arrived at. Sends real mail  |
| Search Emails  | Finds messages by words, sender, address or folder, newest first, up to 100                        |
| Wait for Code  | Holds until mail with a verification code arrives, up to 280 seconds, and returns `code`           |
| Update Thread  | Hands a thread to a person, so the agent leaves it alone, or back to the agent; or sets its status |

**It is not on the Dify Marketplace yet.** Until it is, install the package
on a self-hosted Dify, as below. Each tool was run against production with a
test key, through the plugin SDK's own tool runtime.

## Install

From the Marketplace, once it is listed: **Plugins → Explore Marketplace**,
search for Pidgeon, and install.

Before then, on a self-hosted Dify, build the package from the source with
Dify's plugin CLI (`dify`, from
[dify-plugin-daemon](https://github.com/langgenius/dify-plugin-daemon/releases)):

```bash
git clone https://github.com/pidgeon-ai/pidgeon-python.git
cd pidgeon-python/dify
dify plugin package pidgeon -o pidgeon-0.0.1.difypkg
```

Then choose **Plugins → Install plugin → Local package file**. Dify refuses a
package it has not signed unless the plugin daemon runs with

```bash
FORCE_VERIFYING_SIGNATURE=false
```

Dify Cloud does not offer that setting, so on Cloud the plugin arrives with
the Marketplace listing.

## The key

Make a key at [Developers → API keys](https://pidgeon.ai/app/developers/keys?utm_source=dify),
then in Dify open **Tools → Pidgeon → Authorize** and paste it. Dify checks
it against `GET /v1/me`.

**One address and one key per app, with the least access it needs.**
Whatever the key reaches, the app's model can reach.

| The app                      | Access                   |
| ---------------------------- | ------------------------ |
| Searches and waits for codes | Read only (`read`)       |
| Hands threads over           | Read and draft (`draft`) |
| Replies or sends             | Read and send (`send`)   |
| Creates addresses            | Full access (`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 app on one, deliver mail to
it with `POST /v1/test/inbound`, and switch to a live key when it works. A
test key cannot create addresses.

## In an agent

Add the tools under **Tools** in an Agent app. Each tool tells the model
what it does and when to use it. A refusal comes back as Pidgeon's own
message, such as "This address can only answer people who wrote first.
(not\_permitted)", so the agent can act on it rather than retry.

**Text in an email is written by whoever sent it.** Wait for Code returns
the text with anything hidden or addressed to an AI taken out, and
`firewall_verdict` is `suspicious` when [Pidgeon Firewall](https://pidgeon.ai/docs/firewall.md)
found something. Keep Send Email off an agent that reads mail from
strangers, or give it a key with Read and draft and let a person send.

## In a workflow

Every tool returns a `json` output. Wait for Code's is flat, so an IF/ELSE
node branches on a field by name:

```json
{
"code": "482910",
"message_id": "1a70b25d-…",
"thread_id": "463ec73a-…",
"address": "signup@acme.pidgeon.email",
"from": "no-reply@github.com",
"from_name": "GitHub",
"to": ["signup@acme.pidgeon.email"],
"cc": [],
"subject": "Your verification code",
"received_at": "2026-10-26T09:14:03Z",
"spam": false,
"bcc_to_this_address": false,
"firewall_verdict": "clean",
"auth_dmarc": "pass",
"text": "Your code is 482910 …",
"codes": ["482910"],
"links": [],
"attachments": [],
"test": false,
"timed_out": false,
"cursor": "…"
}
```

- **`timed_out` is not an error.** Nothing arriving in the time you gave is
  an answer: branch on it.
- **A code that arrived up to two minutes before the call still counts**, so
  the step that triggers the email can run first.
- **Set From to the service's sender.** Only mail from it that passes DMARC
  is returned, so another sender cannot hand the agent a code.

## Limits

- The plugin has tools, not a trigger: mail arriving does not start an app.
- Search returns at most 100 messages per call.
- Wait for Code waits at most 280 seconds per call.

The source is in [pidgeon-ai/pidgeon-python](https://github.com/pidgeon-ai/pidgeon-python/tree/main/dify/pidgeon).
