Skip to content

Automation & agents

The four AI surfaces in Tinbox — the interactive assistant, autopilot, dedicated agents, and agentic skills — plus how the assistant drives your inbox over chat, MCP, and the CLI through one shared tool manifest.

Tinbox is built agent-first, human-supervised. There are four distinct AI surfaces, and the single biggest thing to get right is telling them apart. They all stand on the same shared tool manifest, but they differ in who drives and when they run.

The four surfaces

SurfaceWhat it isConfigure with
AssistantThe interactive chat agent (in-app chat, /mcp, CLI) a human drives, turn by turn.config.assistant
AutopilotThe assistant put on a cron with standing orders — it works the inbox on a schedule, within the tools you opt it into.config.autopilot + Settings → Autopilot
AgentsDedicated autonomous agents, each bound to one inbox, with their own prompt + tool allowlist + optional human-in-the-loop.config.agents via defineAgents({...})
Agentic skillsThe tools the three above call — an agenticSkill() you author.config.skills (see Skills)

Put simply: the assistant is interactive (you drive it); autopilot is that same assistant on a timer with a brief; agents are separate autonomous workers each owning an inbox; and an agentic skill is a tool any of them invoke. A defineAgents agent and autopilot can both run unattended, so each gates outbound sends behind approval (HITL) — irreversible actions stay human-gated.

// tinbox.config.ts — the surfaces are separate config keys
import { defineAgents } from "tinbox/cf/agents";

const config: TinboxConfig = {
  assistant: { systemPrompt: "Be concise; cite thread ids." },   // interactive
  autopilot: {                                                   // the assistant on a cron
    enabled: true,
    instructions: "Triage overnight; draft replies; never send.",
    tools: ["search_emails", "summarize_thread"],
    principal: "owner",                                          // who it acts as (default)
  },
  agents: defineAgents({                                         // a dedicated agent per inbox
    supportBot: {
      inboxId: "inbox_support",
      name: "Support Bot",
      systemPrompt: "You are a helpful support agent…",
      tools: ["search_emails", "create_draft"],
      hitl: { required: true, ttl: 3600, expirationAction: "reject" },
    },
  }),
  // skills: defineSkill([...]) — the agentic tools all of the above call
};

For the full decision map ("I want X → use Y"), see Core concepts → Extend & automate.

Who autopilot acts as — the designated principal

The interactive assistant runs with your grants — the session user driving it. Autopilot has no session, so when it calls a core inbox tool (search, list, summarize — the ones that authorize per-grant) it needs an identity to act as. You designate one with autopilot.principal:

autopilot: {
  enabled: true,
  tools: ["search_emails", "summarize_thread"],
  principal: "owner",                       // ← the default
  // principal: { user: "user_abc" },        // a specific user's grants
  // principal: { tokenEnv: "AUTOPILOT_TOKEN" }, // a scoped API key (least privilege)
}
  • "owner" (default) — the loop runs as the org super-user (you, the deployer), so it reaches every inbox you can. Zero config; a fresh deploy just works.
  • { user } — runs as a specific user, scoped to that user's inbox grants.
  • { tokenEnv } — runs as an API key read from a Settings → Secrets variable. Least privilege: mint a tbx_… key (Settings → API keys) granted only the inboxes and scopes the loop needs, store it as a secret, and name the variable here. Only the variable name lives in your config — never the key itself.

Autopilot's reach is then exactly that principal's reach, and the audit trail attributes its actions to a real identity. Two guarantees hold regardless of which principal you pick:

  • It proposes; a human commits. Autopilot may run any reversible tool — reads (search/summarize/list), drafting (create_draft/update_draft), contact updates (upsert_contact, tag_contact, … — the registry has undo), and campaign planning (create_campaign/update_campaign/create_template). Anything irreversible — send_email, send_campaign, schedule_campaign, approve_campaign, remove_subscriber, forwarding/notification dispatch — is always held for human approval, even when the principal could do it.
  • No principal ⇒ no core tools. If the principal can't resolve (e.g. nobody has signed in yet, or the token secret is unset), autopilot simply skips core tools that tick and logs why — it never falls back to running unauthorized. Your reversible tenant skills still run, since they carry their own auth.

You can see exactly what it did each run in Settings → Autopilot → Recent activity — the last runs, their summaries, the tools each touched, and how much was held for you.

principal lives in tinbox.config.ts (git-reviewed), not the runtime Settings — changing who the autopilot is, is a privilege decision. Settings still own its behavior (on/off, tools, brief).

The rest of this page covers the assistant surface — how it (and the MCP server and CLI) drives your inbox through the shared tool manifest.

One manifest, three ways to reach the assistant

The companion app is the supervision surface, not the only way in. Three surfaces let you (or an agent) operate your inbox through the assistant, and they all share one tool manifest, so a tool behaves identically wherever it is called.

Every capability is declared once as a tool, then exposed on three surfaces:

  • The in-app chat assistant. A conversational panel in the companion app that operates the inbox through tools. It runs with the session user's grants, so it can never see or do more than the human driving it.
  • The MCP server. Your deployment exposes a Model Context Protocol endpoint at /mcp. The Claude app, Claude Code, or any MCP agent connects and gets the same tools. See Connect an MCP client.
  • The tinbox CLI. The agent's hands in a terminal. A coding agent in any repo can search your mail, draft, and send without an SDK integration. The CLI is a thin veneer over the same API the app uses, so it gets no special powers.

Whichever surface calls a tool, the request funnels through one authorize(caller, inbox, action) check against the caller's grants. The chat adds zero new data-access paths. Browse every tool in the tool reference.

Read-only vs. destructive tools

Each tool carries annotations. Read-only tools (search, summarize, list) are safe to auto-run: search_emails, get_thread, summarize_thread, list_threads. Destructive tools write or send and are flagged so the surface asks first: send_email and the campaign-send path.

  • In chat and MCP, a destructive tool is marked so the client surfaces a confirmation before it runs. Sending stays a human decision.
  • In the CLI, a destructive command requires an explicit --yes. Without it, the command refuses rather than guessing.

This mirrors the storage boundary: irreversible actions stay human-gated, reversible ones stay permission-gated. An agent physically cannot escalate past either.

The CLI as the agent's hands

The CLI emits machine-readable output with --json on every command and meaningful exit codes, so an agent parses results and branches on failures precisely. Auth is a capability token (tbx_…) via TINBOX_TOKEN or a config file. A few of the commands an agent reaches for:

tinbox search "invoice overdue" --inbox support --json
tinbox threads --unanswered --json
tinbox draft create --thread thr_… --text "On it, sending today." --json
tinbox send --to ada@example.com --subject "Update" --text "..." --json

The freshness probe (tinbox threads --changed-since and tinbox draft version) lets a long-running or branch-based agent check whether state moved before it acts, and a draft update is a compare-and-swap that rejects a stale write with a conflict the agent can rebase on.

Agents can read these docs

The MCP server ships a built-in, read-only search_docs tool. An agent connected to your deployment can search these very guides and the tool reference at action time. Ask it: "what params does send_email take?" and it answers from the reference without you pasting anything.

Next steps