Extend: Parsers — read, classify, and render inbound mail
Lane 1 of Extend Tinbox — reactive skills that classify inbound mail and render a card, plus the in-app skill-draft playground.
A parser is a reactive-mail skill: it reads an inbound message, extracts what it can, and hands back a verdict — a category, some flags, queryable fields, and the id of a card the inbox should render. It never acts; it only annotates. This is Lane 1 of the six-lane Extend Tinbox series, and the safest place to start, because a parser can only ever add reversible metadata to a message.
A complete, runnable parser
Drop this in tinbox.config.ts (or a file it imports) and it runs on every inbound message. It pulls an order number out of the body and tells the inbox to render an order-card inline:
import { definePlugin } from "tinbox/cf/plugin";
import type { AlgorithmicSkill, SkillVerdict } from "tinbox/cf/skills";
const orderParser: AlgorithmicSkill = {
id: "order-number",
label: "Order confirmations",
kind: "algorithmic",
appliesTo: "message",
tagline: "Spot order confirmations and surface the order number.",
match(ctx): SkillVerdict | null {
// ctx.message.from, ctx.message.subject, ctx.message.text are available.
const text = `${ctx.message.subject}\n${ctx.message.text}`;
const m = text.match(/order\s*#?\s*([A-Z0-9]{5,})/i);
if (!m) return null;
return {
category: "orders",
flags: ["awaiting-fulfilment"],
extra: { orderNumber: m[1] }, // → queryable custom fields on the message
component: "order-card", // → a @tinbox/ui component id the SPA renders inline
};
},
};
Then register it (see Registering a parser below):
// tinbox.config.ts
import { defineConfig } from "tinbox/cf";
import { defaultSkills } from "tinbox/cf/skills";
export default defineConfig({
skills: [...defaultSkills(), orderParser],
});
That's the whole loop: a message arrives, match() runs, and if it returns a verdict the message is tagged orders, flagged awaiting-fulfilment, gains a queryable orderNumber field, and shows an order card. Return null and the skill simply abstains.
The verdict — annotations only, never side effects
A reactive-mail skill is reversible-only. Everything it can do is expressed in the SkillVerdict it returns:
| Field | What it does |
|---|---|
category | Files the message under a category (drives filtering + views). |
flags | Adds one or more flags — badges the inbox can filter and act on. |
extra | An object that becomes queryable custom fields on the message. |
component | Names a @tinbox/ui component id to render inline on the message. |
draft | Suggests a reply draft the operator can accept or discard. |
Every field is optional and every effect is metadata layered onto the message — nothing is sent, deleted, or pushed to another system. A parser that returns null did nothing at all.
If you need to actually do something — write to a CRM, hit a webhook, kick off a workflow — that is not a parser. That's a Plugin onEmail hook, which is Lane 4 — Plugins. Parsers classify; plugins act.
component ships an id, not React
The component field is a string id, not a React component. The engine has no idea what the card looks like — it just records "order-card" on the message, and the inbox SPA looks that id up in @tinbox/ui and renders it inline, hydrated from the message plus your extra fields.
Built-in card ids you can reuse from your own parsers include order-card and otp-quick-copy. Keeping the id on the engine side and the React on the SPA side is what lets a parser run inside the Worker (and inside the playground, below) without ever evaluating UI code.
Built-in parsers — spread and extend
Tinbox ships a set of reactive parsers so common mail is understood out of the box:
otp— one-time passcodes (rendersotp-quick-copy)shipping— carrier tracking + delivery statusreceipt— purchase receipts and totalscalendar-invite— ICS invitesflight— itineraries and boarding infoshopify-order— Shopify order confirmations
Pull them in with defaultSkills() and append your own — order matters only for card selection, and each skill abstains independently:
import { defaultSkills } from "tinbox/cf/skills";
skills: [...defaultSkills(), orderParser];
Omit defaultSkills() and you opt out of every built-in — usually you want to spread it.
Registering a parser
There are two places a parser can live:
-
config.skillsintinbox.config.ts— the direct route shown above. Best for tenant-specific parsers. -
A plugin's
skills:array — bundle parsers with related hooks and views so they install as one unit:export default definePlugin({ id: "acme-orders", skills: [orderParser], });
Either way the parser joins the same reactive pipeline. For the full defineSkill surface — verdict shape, appliesTo targets, and every field — see Skill authoring.
The skill-draft playground (agent-authored parsers)
You don't have to write TypeScript to draft a parser. In the inbox, go to Settings → Skills → Playground:
- Write a structured, non-TS skill spec (an agent is good at this).
- Paste a real email — the actual message you want to classify.
- Run it live and watch the extracted fields, category, flags, and chosen card appear.
Nothing is deployed and no code is evaluated — the playground interprets the structured spec against your pasted email, so an agent can iterate on a parser in seconds, see exactly what it would extract, and only then promote it into config.skills. This is the fast path for agent-authored parsers: draft, paste, run, refine — no redeploy.
Which lane instead?
A parser reads and annotates. If that's not what you need:
- You need to act on mail — call a CRM, fire a webhook, run a workflow → Lane 4 — Plugins (
onEmailhooks do side effects). - You need the assistant to call a lookup — pull live data on demand from a chat turn → Lane 5 — Views / agent tools.
Back to the hub: Extend Tinbox.