Example: support triage
For: plugin authors.
The examples/jev-tools plugin shows tools, agents, a flow, a hook and evals working together. Jev, the service from TypeSafe AI that answers typed questions with a confidence value, chooses a support ticket's team and assesses refund risk.
The example currently requires access to the private repository; public source downloads are coming soon. From your checkout, run make demo-m5 to exercise the complete example.
jev-tools/
├── oz0-plugin.yaml # tools, the hook, requires.secrets: [JEV_API_KEY]
├── src/jev_tools/
│ ├── jev_mcp.py # jev.decide: typed questions in, answers with a confidence out
│ ├── hooks.py # jev_guard.check_refund: is this refund risky?
│ └── support_mcp.py # jev_support.lookup_order, jev_support.refund
├── agents/ # jev-billing, jev-technical, jev-sales
├── flows/jev-triage.yaml # started by Zendesk's webhook
└── evals/ # billing.yaml, triage.yaml
The flow
name: jev-triage
input_schema: schemas/ticket.json
trigger:
webhook: { secret: ZENDESK_WEBHOOK }
steps:
- id: decide
tool: jev.decide
input:
state: ${{ input.ticket }}
questions:
team: { choice: [billing, technical, sales] }
urgent: { yes_no: "The customer needs an answer within an hour" }
- id: route
when: steps.decide.team.confidence >= 0.8
agent: jev-${{ steps.decide.team.choice }}
input: ${{ input.ticket }}
- id: ask_human
when: steps.decide.team.confidence < 0.8
approval:
details: 'Jev is not sure where "${{ input.ticket.subject }}" goes. Send it to ${{ steps.decide.team.choice }} (confidence ${{ steps.decide.team.confidence }})?'
timeout: 4h
- id: route_checked
when: has(steps.ask_human)
agent: jev-${{ steps.decide.team.choice }}
input: ${{ input.ticket }}
output: "${{ has(steps.route) ? steps.route : steps.route_checked }}"
A ticket Jev is sure about goes straight to that team's agent; one it is unsure about waits until a person approves its guess. See Flows.
A ticket is outside text, so the flow is untrusted from its start, and jev.decide sends the ticket to Jev, outside the cluster: the taint-default rule asks a person before that call, and the card says the taint asked. A tenant that trusts its triage service with its tickets narrows the rule for that one call, with a reason, as make demo-m5 does: oz0 tenant tool-policy set --taint-default-when 'job.tainted && tool.outbound && !(mcp.tool.target == "jev" && mcp.tool.name == "decide")' --reason "...". The job stays tainted, so its other outbound calls still ask.
The tool, its key and its cost
jev.decide reads the service's key from the environment: the plugin declares requires.secrets: [JEV_API_KEY], so every process of the plugin gets the tenant secret, and the key never goes into the repository. It returns the answers as structured content and reports what the service charged in the result's _meta:
return CallToolResult(
content=[TextContent(type="text", text=json.dumps(answers))],
structured_content=answers,
_meta={"oz0/usage": {"service": "jev", "unit": "question", "units": len(questions), "cost_usd": cost}},
)
That cost lands on the job next to the model calls, and counts toward the job's budget:
JOB AGENT NODES MODELS CALLS INPUT CACHED OUTPUT COST
hook-jev-triage-zendesk-ticket-1001 jev-triage node-be9581dcfec6 0 0 0 0 $0.0004
└ jev-billing-2227b26b jev-billing node-be9581dcfec6 anthropic/claude-haiku-4-5 3 1211 0 73 $0.0018
Reported by plugins:
hook-jev-triage-zendesk-ticket-1001 (jev-triage): jev, 2 questions, $0.0004, by jev-tools
jev-billing-2227b26b (jev-billing): jev, 1 question, $0.0002, by jev-tools
See Secrets for plugins and Report what your own services cost.
The hook
Before every refund, jev_guard.check_refund asks Jev whether the refund is risky. A risky one (here: over $100) is escalated, so the call waits for a person:
hooks:
- on: pre_tool_call
call: jev_guard.check_refund
match: {tools: [jev_support.refund]}
jev-billing → jev_support.refund {"amount_usd":400,"order_id":"5678","reason":"double charge"}
jev-billing ⏸ jev_support.refund waits for approval a-86f882b36825: {"hook": "jev_guard.check_refund", "reason": "Jev thinks this refund is risky (0.90)", ...}
jev-billing ▶ jev_support.refund approved by operator:admin: checked the bank statement
jev-billing ← jev_support.refund Refunded $400.00 for order 5678 (double charge).
When Jev cannot be reached, the hook escalates too, so a person decides rather than nobody. See Hooks.
The evals, and a version that does worse
evals/billing.yaml wants a double charge refunded and a single charge left alone; evals/triage.yaml sends a ticket through the whole flow. plugin test runs them while the team works, and the quality gate runs them again at every install.
In the demo, a teammate's version 1.1.0 drops jev_support.refund from the billing agent's tools. Its evals get worse, and the gate stops it before the fleet gets it:
Gate of jev-tools 1.1.0: stopped: eval billing: 1 of 2 cases passed (50%), below its threshold of 100%
The fleet keeps answering tickets with 1.0.0.
Run it
make demo-m5 # stand-ins for Jev and Claude, no keys needed
ANTHROPIC_API_KEY=sk-ant-... make demo-m5 # the agents on Claude (claude-haiku-4-5)
scripts/fakejev answers it in the demo. With access to Jev, replace ask() in jev.py with a call to TypeSafe's SDK; the same pattern works with a classifier of your own or a small LLM.The build kit
Skills that teach your coding agent to build Orchestrator Zero plugins the way verified plugins are built, installed with oz0 setup for Claude Code, Codex, Gemini CLI, Cursor, GitHub Copilot, OpenCode and goose.
Publish and version
Ship a plugin with a Git tag, and control exactly what every node runs.