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 functionhandle(event, ctx) (sync or async) and return a verdict object.
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:
event.payload fields
For inbound email:
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:
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.
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
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:
Examples
Drop bulk / newsletter mail before the classifier (saves the LLM call)
File receipts in the Inbox without triggering a reply
Auto-acknowledge with a canned reply — no LLM (run_tool)
Send the reply as your connected Gmail / Outlook identity
Block a domain / file by keyword (Python)
Generic: gate on the payload, then decide
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:
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.