> ## Documentation Index
> Fetch the complete documentation index at: https://agentheya.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Event pre-processor

> Run your own code in a sandbox before the agent (and before any LLM classifier) sees an incoming event, and decide what happens next.

The **event pre-processor** is optional owner-supplied code that runs in an isolated sandbox **before** the agent — and before any LLM classifier — sees an incoming event (an inbound email, a webhook, a feed item, an IM message, an MCP event). It returns a **verdict** that decides what happens next.

Because it runs first, a `drop` here happens **before**, and skips, the more expensive LLM classifier — so the pre-processor is the cheapest place to filter bulk mail, health-check pings, or anything you never want the agent to act on.

## The contract

Define a function `handle(event, ctx)` (sync or async) and **return a verdict object**.

```javascript theme={null}
function handle(event, ctx) {
  if ((event.payload?.amount ?? 0) < 100)
    return { action: 'drop', reason: 'below threshold' };
  return { action: 'pass_to_agent', system_hint: 'High-value event' };
}
```

```python theme={null}
def handle(event, ctx):
    if event['payload'].get('amount', 0) < 100:
        return { 'action': 'drop', 'reason': 'below threshold' }
    return { 'action': 'pass_to_agent', 'system_hint': 'High-value event' }
```

Your code runs in an isolated sandbox (Python 3.11 or Node 20, \~512 MiB, \~30 s). If your code throws, returns nothing, or returns something unrecognized, the platform **fails open** to the default verdict (`pass_to_agent`) — your event is never silently lost. Only an explicit, successful `drop` discards it.

Keep your logic self-contained on the `event`. Console output (`console.log` / `print`) is captured into the pre-processor audit row.

## The `event` object (1st argument)

Always present:

| Field                                                          | Description                                                                     |
| -------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| `event.event_id`                                               | Unique id for this occurrence (idempotency key)                                 |
| `event.source`                                                 | The ingress kind: `email` / `webhook` / `feed` / `im` / `mcp`                   |
| `event.source_name`                                            | Human-meaningful source label (for email, the inbound address that received it) |
| `event.event_name`                                             | The event type string (for email, `"email.received"`)                           |
| `event.occurred_at`                                            | ISO-8601 timestamp                                                              |
| `event.sender`                                                 | `{ id, display?, verified? }` (for email, `id` is the sender address)           |
| `event.agent_id`, `event.owner_user_id`, `event.power_user_id` | The matched subscriber, or `null`                                               |
| `event.payload`                                                | The message body (see below)                                                    |
| `event.context`                                                | Ingress extras — headers, method, channel id, … (not set for email today)       |

### `event.payload` fields

For **inbound email**:

| Field                   | Description                           |
| ----------------------- | ------------------------------------- |
| `event.payload.from`    | Sender, e.g. `"Jane <jane@acme.com>"` |
| `event.payload.to`      | The inbound address that received it  |
| `event.payload.subject` | The subject line (string)             |
| `event.payload.body`    | The plain-text body (string)          |

For other ingresses, `event.payload` is the parsed event body — a JSON object when parseable, else the raw string.

## The `ctx` object (2nd argument)

`ctx` lets your code reach back into the platform **while it decides** — to look up prior mail from a sender, check a label, or send an acknowledgement before returning a verdict. This is available on **every** ingress (email, webhook, feed, IM, MCP).

Unlike a `run_tool` verdict — which is terminal and discards the tool's result — `ctx.tools.*` **returns the result to your code**, so read/search tools are genuinely useful here.

### `ctx.tools.<name>(input)` → result

Calls a built-in tool with no LLM in the loop, over the **same** execute path the agent uses (same validation, same billing). Returns the tool's result, or an `{ error, details }` envelope on failure (e.g. when a mailbox isn't connected — your code keeps running). Available tools:

| Tool                                                                | Use                                                                                                                                           |
| ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `email_read`, `email_status`                                        | Look up recent inbound mail / delivery status (platform).                                                                                     |
| `email_send`                                                        | Send via the platform address (recipients locked for subscriber sessions).                                                                    |
| `gmail_search`, `gmail_read_message`, `gmail_list_labels`           | Read your connected Gmail.                                                                                                                    |
| `gmail_send`, `gmail_create_draft`, `gmail_modify_labels`           | Act on your connected Gmail.                                                                                                                  |
| `outlook_search`, `outlook_read_message`, `outlook_list_categories` | Read your connected Outlook.                                                                                                                  |
| `outlook_send`, `outlook_create_draft`, `outlook_organize_message`  | Act on your connected Outlook.                                                                                                                |
| `show_image`, `show_file`                                           | Get a public CDN/file **URL** for an agent file — embed it in a reply.                                                                        |
| `surface_file`                                                      | Generate a real, resolvable **download link** for a file.                                                                                     |
| `render_ui`                                                         | Build a json-render UI spec. Note: only the live agent chat renders it — on email/IM the spec is data, so prefer `pass_to_agent` for real UI. |
| `web_search`, `web_fetch`                                           | Search the web / read a URL.                                                                                                                  |
| `calculate`                                                         | Evaluate a math expression exactly.                                                                                                           |
| `execute_code`                                                      | Run a Python/JS snippet in its own sandbox and get the output.                                                                                |
| `record_power_user_memory`, `find_memories_by_entity`               | Write / look up this sender's memory.                                                                                                         |
| `get_user_memory`, `update_user_memory`, `forget_user_memory`       | Read / edit structured user memory.                                                                                                           |
| `knowledge_graph_query`                                             | Query the agent's knowledge base (RAG).                                                                                                       |

These run the **same** execute path as the agent — same validation, billing, killswitches, and flood limits apply whether the LLM or your code calls them. The file tools return a **URL / spec your code can use** (e.g. drop a download link into an `email_send` / `gmail_send` reply); they create no interaction rows.

```javascript theme={null}
async function handle(event, ctx) {
  // Look something up before deciding — ctx.tools returns the result to you.
  const recent = await ctx.tools.email_read({ from: event.payload.from, days: 30, limit: 3 });
  console.log('prior messages from this sender:', recent);
  return { action: 'pass_to_agent', system_hint: 'See prior thread.' };
}
```

### `ctx.audit(name, data?)`

Writes a structured line to the pre-processor audit log — handy for recording *why* your code decided something.

A pre-processor may make at most **50** `ctx.tools` / `ctx.audit` calls per run (a guard against runaway loops). The tools that are **not** here are the ones that need a live agent run — **interactive UI** (`collect_form`, `ask_question`, or an interactive `render_ui`, which need a chat screen + answer round-trip), and run-scoped tools like `request_human_handoff`, `delegate_to_agent`, or `mark_goal_complete`. For those, return a `pass_to_agent` verdict and let the agent drive them.

The standard runtime is also available: `console.log`, `JSON`, `Math`, `Date`, `Buffer`, `fetch` (Node 20); or `print`, `json`, `re`, `urllib`, `asyncio` and the Python 3.11 stdlib.

## Verdicts you can return

| Verdict                                               | Effect                                                                                                                           |
| ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `{ action: 'pass_to_agent' }`                         | The default. Hands the event to the normal flow (for email: Inbox mirror + classifier / reply).                                  |
| `{ action: 'drop', reason? }`                         | Discard. For email: no Inbox, no classifier, no reply.                                                                           |
| `{ action: 'store', reason? }`                        | For email: mirror to the Inbox so you see it, but do not reply. Otherwise: keep for audit without invoking the agent.            |
| `{ action: 'run_tool', tool, input? }`                | Run **one** allow-listed tool directly, no LLM. Terminal — skips the classifier. See [Running a tool](#running-a-tool-run-tool). |
| `{ action: 'respond', body, status?, content_type? }` | Synchronous HTTP reply (for webhooks; ignored for email).                                                                        |

On `pass_to_agent` the optional `payload` / `system_hint` / `chat_session_strategy` fields are accepted, but not yet applied on the email path (reserved).

### Running a tool (`run_tool`)

`run_tool` runs a single tool and stops — its result is **not** returned to your code, so read/search tools are pointless here. The tool acts on the **owner's** connected mailbox; if no mailbox is connected the tool returns a "connect a mailbox" error (logged to the audit row) and the email otherwise proceeds normally. Allowed tools:

| Tool                                              | Use                                                                                                                              |
| ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `email_send`                                      | Send a reply **via the platform address**.                                                                                       |
| `gmail_send`, `gmail_create_draft`                | Send / draft **as your connected Gmail** (connect it on the Email page).                                                         |
| `outlook_send`, `outlook_create_draft`            | Send / draft **as your connected Outlook**.                                                                                      |
| `gmail_modify_labels`, `outlook_organize_message` | Tag / archive / move — needs a real mailbox **message id** (see [Tagging & moving](#tagging-moving-and-richer-mailbox-actions)). |
| `email_read`, `email_status`                      | Available, but rarely useful via `run_tool` (terminal).                                                                          |

## Examples

### Drop bulk / newsletter mail before the classifier (saves the LLM call)

```javascript theme={null}
function handle(event, ctx) {
  const { from, subject, body } = event.payload;
  // This runs BEFORE the LLM classifier — a drop here skips it entirely.
  if (/no-?reply|newsletter|notifications?@/i.test(from))
    return { action: 'drop', reason: 'bulk / no-reply sender' };
  if (/unsubscribe/i.test(body) && !/invoice|urgent/i.test(subject))
    return { action: 'drop', reason: 'looks like a newsletter' };
  return { action: 'pass_to_agent' };   // default: hand it to the agent
}
```

### File receipts in the Inbox without triggering a reply

```javascript theme={null}
function handle(event, ctx) {
  const subject = (event.payload.subject || '').toLowerCase();
  if (subject.includes('receipt') || subject.includes('invoice'))
    return { action: 'store', reason: 'receipt filed — no reply needed' };
  return { action: 'pass_to_agent' };
}
```

### Auto-acknowledge with a canned reply — no LLM (`run_tool`)

```javascript theme={null}
function handle(event, ctx) {
  const { from, subject } = event.payload;
  if (/^(out of office|automatic reply)/i.test(subject || ''))
    return {
      action: 'run_tool',
      tool: 'email_send',   // allow-listed: email_send | email_read | email_status
      input: {
        to: from,
        subject: 'Re: ' + (subject || ''),
        body_text: 'Thanks — your message is logged; we will follow up.',
      },
    };
  return { action: 'pass_to_agent' };
}
```

### Send the reply as your connected Gmail / Outlook identity

```javascript theme={null}
function handle(event, ctx) {
  const { from, subject } = event.payload;
  if (/^(out of office|automatic reply)/i.test(subject || ''))
    return {
      action: 'run_tool',
      tool: 'gmail_send',   // sends from your real Gmail; swap for 'outlook_send' or 'email_send'
      input: {
        to: from,
        subject: 'Re: ' + (subject || ''),
        body_text: 'Thanks — your message is logged; we will follow up shortly.',
      },
    };
  return { action: 'pass_to_agent' };
}
```

### Block a domain / file by keyword (Python)

```python theme={null}
def handle(event, ctx):
    p = event['payload']
    sender = (p.get('from') or '').lower()
    if sender.endswith('@blocked-domain.com'):
        return { 'action': 'drop', 'reason': 'blocked domain' }
    if 'receipt' in (p.get('subject') or '').lower():
        return { 'action': 'store' }          # keep it, don't reply
    return { 'action': 'pass_to_agent' }
```

### Generic: gate on the payload, then decide

```javascript theme={null}
function handle(event, ctx) {
  // event.payload is the parsed body (JSON object or raw string).
  if (event.event_name === 'ping')
    return { action: 'drop', reason: 'health check' };
  return { action: 'pass_to_agent' };
}
```

## Tagging, moving, and richer mailbox actions

`gmail_modify_labels` / `outlook_organize_message` act on a specific message and need its **mailbox message id**. An email arriving at your **platform inbound address** is forwarded and isn't in your mailbox, so the `event` itself carries no id — but with `ctx.tools` you can look the message up in your connected mailbox and act on it **inline, before returning a verdict**:

```javascript theme={null}
async function handle(event, ctx) {
  // Find this email in the connected Gmail mailbox, then label + archive it.
  const found = await ctx.tools.gmail_search({
    query: `from:${event.payload.from} newer_than:1d`,
    max_results: 1,
  });
  const msg = found?.messages?.[0];
  if (msg) {
    await ctx.tools.gmail_modify_labels({
      message_id: msg.id,
      add_label_ids: ['Label_Finance'],   // resolve names via ctx.tools.gmail_list_labels()
      archive: true,
    });
    return { action: 'drop', reason: 'auto-filed to Finance' };
  }
  return { action: 'pass_to_agent' };
}
```

`ctx.tools` requires a connected Gmail/Outlook mailbox; each tool returns an `{ error }` envelope (not a throw) when none is connected, so your code keeps running. The same pattern works with `outlook_search` → `outlook_organize_message`.

You can also simply hand it off — return `pass_to_agent` and tell the agent how to file the email in the address's **Context** field or your **Rules** — when you'd rather the LLM decide.
