Extend: Views — dashboard tiles, cards, and agent tools
Lane 5 of Extend Tinbox — config-registered UI surfaces (plugin.ui slots) plus tenant-authored agent tools (plugin.agentTools), both deploy-time and declarative.
A View is how you reshape the app surface: render your own component into a
named slot, and give the assistant the query behind it. Lane 5 has two halves —
the UI side (plugin.ui, where a component renders) and the data side
(plugin.agentTools, a function the assistant can call). Both are deploy-time
and declarative: the engine ships a component id and the SPA owns the code, so
there is no runtime code upload. (A runtime, D1-stored view builder is a
separate cloud-track feature, not this lane.)
Here is a plugin that does both — a "sourced this week" dashboard tile backed by an agent-callable query:
import { definePlugin } from "tinbox/cf/plugin";
import { agenticSkill } from "tinbox/cf/skills";
import { z } from "zod";
export default function weeklyPulse() {
return definePlugin({
name: "weekly-pulse",
version: "1.0.0",
// The DATA side — a tool the assistant, MCP, and CLI can call. Same mechanism
// as a top-level agentic skill; listing it here reads as "this plugin's tools."
agentTools: [
agenticSkill({
id: "weekly-signups",
label: "Weekly signups",
description: "Count new audience signups in the last 7 days.",
inputSchema: z.object({ audienceId: z.string() }),
run: async ({ audienceId }, ctx) => {
// your query — return model-facing data
return { audienceId, added: 42, window: "7d" };
},
}),
],
// The UI side — render a component id into a named slot. The component must
// already exist in the deployed UI bundle (a builtin, or one you vendored).
ui: [
{
slot: "dashboard",
component: "info-note",
title: "Weekly pulse",
props: { body: "42 new signups this week. Ask the assistant for the breakdown." },
visibility: "ui",
},
],
});
}
Compose it into config.plugins in tinbox.config.ts and redeploy. The tool shows
up wherever the assistant runs (chat, /mcp, CLI); the card renders on the
dashboard.
The UI side — plugin.ui
Each entry declares that a component should render into a named app slot:
ui: [
{ slot: "dashboard", component: "revenue-tile", visibility: "ui", props: { period: "30d" } },
]
slot— one of the named app slots:dashboard,contact-card,inbox-card,message-actions,thread-actions. Keep this list in mind — an unknown slot fails validation atdefinePlugintime.component— the@tinbox/ui-style component id the SPA resolves through its registry. The worker never imports your React; it carries an id. An unknown id renders nothing — a surface pointing at a component this build doesn't have fails soft, it never crashes the page.visibility—ui(human only),model(assistant context only), orboth(default). Only non-modelsurfaces render in a slot.props— static, JSON-serializable configuration passed to the component. For data that must be fetched live, back the component with a pluginroutesendpoint (lane 4) or anagentToolsquery (below) rather than static props.
The composed manifest is read by the SPA via surfaces.list; the slot host renders
each entry through the id→component firewall. The shipped info-note component is a
titled note card you can point at to prove a slot renders (pass props.body); add
your own components to your vendored copy of the surface registry.
The data side — plugin.agentTools
agentTools is a semantic alias for contributing agentic skills — functions the
assistant (and MCP, and the CLI) can call as tools, without forking the core tool
set. Author each with agenticSkill():
import { agenticSkill } from "tinbox/cf/skills";
import { z } from "zod";
agenticSkill({
id: "order-lookup",
label: "Order lookup",
description: "Look up an order by number and return its status.",
requiresEnv: ["SHOP_TOKEN"], // gated off until the secret is set
requiresHosts: ["api.myshop.com"], // ctx.fetch host allowlist
inputSchema: z.object({ orderId: z.string() }),
run: async ({ orderId }, ctx) =>
(await ctx.fetch(`https://api.myshop.com/orders/${orderId}`, {
headers: { authorization: `Bearer ${ctx.env.SHOP_TOKEN}` },
})).json(),
});
- The
inputSchema(Zod) typesrun'sinputand validates every call. requiresEnvgates the tool off until its secret is configured;requiresHostspinsctx.fetchto an allowlist.- Default
reversibility: "reversible"lets autopilot call it freely; mark it"acting"and it's held for approval. - Whether you list a tool under a plugin's
agentToolsor a plugin'sskills, or as a top-levelconfig.skillsentry, it merges the same way — your ownconfig.skillswin on an id conflict.
What's shipped vs what's next
- Shipped (config):
plugin.uislot registration, thesurfaces.listmanifest, thedashboardslot render point, andplugin.agentTools. All deploy-time. - Not this lane: a runtime, in-app "view builder" that stores a view spec in D1 (so a tenant who can't redeploy still builds views). That's deferred to the cloud track; this lane is the code-and-config path.
Which lane instead?
- To classify inbound mail and render a card on the message → Parsers
(lane 1) — a reactive skill's
componentverdict. - To run a webhook, cron, or custom route behind your view → Plugins (lane 4).
- To send mail from another app → Scripts (lane 3).
Back to the six lanes.