Skip to content

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:

FieldWhat it does
categoryFiles the message under a category (drives filtering + views).
flagsAdds one or more flags — badges the inbox can filter and act on.
extraAn object that becomes queryable custom fields on the message.
componentNames a @tinbox/ui component id to render inline on the message.
draftSuggests 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 (renders otp-quick-copy)
  • shipping — carrier tracking + delivery status
  • receipt — purchase receipts and totals
  • calendar-invite — ICS invites
  • flight — itineraries and boarding info
  • shopify-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:

  1. config.skills in tinbox.config.ts — the direct route shown above. Best for tenant-specific parsers.

  2. 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:

  1. Write a structured, non-TS skill spec (an agent is good at this).
  2. Paste a real email — the actual message you want to classify.
  3. 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 (onEmail hooks 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.