Add a capability
A capability is three small additions: a catalogue row, a playbook and a server object. The chat menu, the Prompt Lab, the voice relay and the assistant's own self-description all pick it up from there.
As a worked example we add Diary gaps: find stretches where nothing happened on the claim and say whether they matter. It belongs in the Protect group, next to QA review, and it brings a deterministic tool, because counting days between diary entries is arithmetic the model should not do in its head.
Before you start
Decide three things:
- Which group? Understand, Decide, Protect, Produce or Help. The group decides the file and where it appears in the More menu.
- Does it need a tool? If the task depends on counting, dates, money or cross-checking records, yes. If it is judgement over what the shared readers return, no.
- How hard should it think?
low,mediumorhigh. Leave it unset to use the deployment default.
Steps
-
Add the catalogue row in
packages/shared/src/capabilities.ts, inside the right group:packages/shared/src/capabilities.ts{id: 'diary_gaps',label: 'Diary gaps',description: 'Finds stretches with no activity on the file, and whether any of them breached an SLA or left the customer waiting.',group: 'protect',pinned: false,quickActionMessage: 'Are there gaps in the handling of this claim?',},descriptionis shown in the menu and the Prompt Lab, and read by the assistant when asked what it can do, so write it for a handler.quickActionMessageis what appears in the transcript, so make it read as something a handler would type. Pin only a handful of capabilities; a test checks the catalogue never pins all of them. -
Write the playbook in
api/src/prompts/catalogue.ts, in the matching group array:api/src/prompts/catalogue.ts{id: 'capability.diary_gaps',label: 'Diary gaps',description: 'Playbook for finding periods of inactivity and judging their impact.',capabilityId: 'diary_gaps',defaultText: `Call find_diary_gaps first.For each gap it returns, say:- the dates and length, and what was outstanding at the time (open tasks, unanswered correspondence);- whether it breached the client's SLA, quoting the rule;- whether the customer was left waiting, citing the diary entry or document that shows it.Ignore gaps where nothing was outstanding. Finish with the one gap that most needs explaining on file, and what to note.`,},See Write prompts for the conventions: name the tool to call first, say what good looks like, say how to finish.
-
Add the tool and the capability object in the group file. The tool is a
ChatTool; the capability is one line.api/src/capabilities/protect.tsconst findDiaryGaps: ChatTool = {definition: {name: 'find_diary_gaps',description:'Deterministic check: lists every gap between consecutive diary entries longer than the threshold, with the open tasks due inside each gap. Call this first when reviewing handling pace.',parameters: {type: 'object',properties: { minDays: { type: 'number', description: 'Smallest gap to report, in days. Default 14.' } },},},activity: () => 'Looking for gaps in the diary…',run: (input, { claim, today }) => {const minDays = typeof input.minDays === 'number' ? input.minDays : 14;const dates = [...claim.diary.map((d) => d.date), today].sort();const gaps = [];for (let i = 1; i < dates.length; i++) {const days = daysBetween(dates[i - 1]!, dates[i]!);if (days < minDays) continue;const openTasks = claim.tasks.filter((t) => t.status !== 'done' && t.dueDate > dates[i - 1]! && t.dueDate <= dates[i]!);gaps.push({ from: dates[i - 1], to: dates[i], days, openTasks: openTasks.map((t) => `${t.id} ${t.name}`) });}if (!claim.diary.length) return { result: 'No diary recorded on this claim.' };return { result: JSON.stringify({ today, minDays, gaps, slaDays: claim.client.slaDays }) };},};export const protectCapabilities: Capability[] = [// …{ id: 'diary_gaps', promptId: 'capability.diary_gaps', tools: [findDiaryGaps], thinkingLevel: 'medium' },];Read Add a tool for the rules a tool follows.
-
Test it. Add a
describeblock toapi/src/capabilities/capabilities.test.ts. Build the claim withclaimWithand the record factories, passtodayexplicitly, and assert on the JSON:api/src/capabilities/capabilities.test.tsdescribe('find_diary_gaps (Diary gaps)', () => {it('reports gaps over the threshold with the tasks due inside them', async () => {const claim = claimWith({diary: [diary({ id: 'D1', date: '2026-08-01', content: 'Opened' }), diary({ id: 'D2', date: '2026-08-30', content: 'Chased' })],tasks: [task({ id: 'T1', dueDate: '2026-08-15', name: 'Call claimant' })],});const { gaps } = await runJson<{ gaps: { from: string; days: number; openTasks: string[] }[] }>(toolOf('diary_gaps'), toolContext(claim, '2026-09-05'));expect(gaps).toEqual([{ from: '2026-08-01', to: '2026-08-30', days: 29, openTasks: ['T1 Call claimant'] }]);});it('says plainly when there is no diary', async () => {expect((await toolOf('diary_gaps').run({}, toolContext(claimWith({ diary: [] })))).result).toBe('No diary recorded on this claim.');});});The registry tests already check the new id matches on both sides, the playbook exists, and the tool has a unique name and an object schema.
-
Run the checks.
npm run typecheck && npm test -
Regenerate the reference so the capabilities and tools pages include it:
npm run docs:reference -
Try it. Run
npm run dev, open a claim, pick Diary gaps from More, and refine the playbook in the Prompt Lab against a few claims. Copy the version that works back into the catalogue.
What you do not need to touch
- The chat widget, the capability menu, the Prompt Lab: they read the catalogue.
- The voice relay: it uses the same registry and prompt assembly.
list_capabilities: the assistant describes the new capability from its catalogue row.- The API routes: a capability is just an id on
POST /api/chat.
If the server and shared halves disagree, the Worker refuses to start with Capability catalogue mismatch, and the test suite fails before that.
Optional: let a scenario show it off
Add the capability id to the showcases of a scenario in api/src/generator/scenarios.ts, with a gotcha it should find. Generated claims for that scenario then carry it in their demo hint, and GET /api/claims/random?capability=diary_gaps can find one. See Generating claims.