Your first operator
This tutorial builds a real operator against a real connector: backlog-triage, which watches support conversations in Kustomer, finds the ones breaching first-response, and proposes an assignment plus an internal note for each. Along the way you will name the operator, state its outcome in plain language, set a fail-closed guardrail, deploy, run once, and read the first receipts it leaves.
An operator senses a system through connectors, reasons with a base model, and returns a proposed plan. It never acts. The deterministic executor checks each proposed action against your policy and either runs it once or refuses it. If that split is new to you, read Operators first.
Prerequisites
You need the CLI installed and authenticated (see Installation), and at least one connected system for the operator to sense. This tutorial uses cn-kustomer, the live Kustomer connector, bound to the role support. Any connector that exposes the same conversation capabilities works identically.
# confirm you are authenticated and pointed at the right workspace
fibric whoami
# add the Kustomer connector, bound to the role "support"
fibric connectors add kustomer --as support
# confirm the capabilities it exposes
fibric capabilities ls
Note that the operator you are about to write asks for conversations.assign, not for Kustomer by name. Capabilities sit between operators and vendors; swapping Kustomer for another ticketing system later is a binding change, not a rewrite. See Capabilities.
Name the operator
The name is not cosmetic. It prefixes every idempotency key the operator generates and appears on every receipt it leaves, so it is how you will find this operator's work in the audit trail six months from now. Pick a short, stable, kebab-case name that describes the operation, not the team or the quarter. This tutorial uses backlog-triage.
# scaffold operator.ts and policy.yaml in the current project
fibric operators init backlog-triage
Because the name prefixes idempotency keys, renaming a deployed operator means its past actions no longer match new keys. Actions it already took could be proposed and applied again. Treat the name as permanent once the operator has run in production.
Write the outcome in plain language
Open operators/backlog-triage/operator.ts. The goal field is the outcome you want, stated in plain language; it anchors the model's reasoning on every run. The capabilities array is the complete list of things the operator may ask for. Then run() does the operator's whole job: it senses, reasons, and returns a plan. It does not execute anything.
import { defineOperator } from "@fibric/sdk";
export default defineOperator({
name: "backlog-triage",
goal: "Assign every support conversation breaching first-response to an available agent, and leave an internal note explaining why.",
// the complete list of capabilities this operator may request
capabilities: [
"conversations.read",
"conversations.assign",
"conversations.note",
],
async run(ctx) {
// SENSE: open conversations that have breached first-response
const breaching = await ctx.support.read({
status: "open",
sla: "first_response_breached",
});
// REASON: the model ranks urgency and picks an assignee per conversation
const triage = await ctx.reason({
input: breaching,
instruction: "For each conversation, choose the best available agent and write a one-line reason.",
});
// PROPOSE: return a plan. the operator never acts; the executor disposes.
return ctx.propose(triage.flatMap(t => [
{ capability: "conversations.assign", args: { id: t.conversationId, assignee: t.agentId } },
{ capability: "conversations.note", args: { id: t.conversationId, body: t.reason } },
]));
},
});
Nothing in this file can touch Kustomer. The return value of run() is a proposed plan, a list of actions the operator would like taken. Whether any of them happen is decided in the next step.
Set a guardrail
The policy is the deterministic half of the loop. It is fail-closed: any proposed action with no matching allow rule is blocked, which is the kernel's default behavior, not a convention. In the reference implementation, trust.ts evaluates every side-effecting action and returns BLOCK when no policy matches. Edit operators/backlog-triage/policy.yaml.
# fail-closed: anything not allowed here is refused
version: 1
allow:
- conversations.assign
- conversations.note
limits:
conversations.assign:
max_per_run: 20 # cap the blast radius of any one run
single_flight: by_conversation_id # one in-flight action per conversation
require_receipt: true
Each line maps to a kernel primitive:
| Rule | What it enforces |
|---|---|
allow |
Only conversations.assign and conversations.note may execute. Anything else the model proposes, including reads escalated to writes, is blocked before it reaches the connector. |
max_per_run |
A single run can assign at most 20 conversations. A runaway plan that proposes 500 assignments is truncated at the cap, not executed. |
single_flight |
At most one in-flight side effect per conversation. Two concurrent runs cannot both act on cnv-18841; the second waits for the first. See Single-flight & idempotency. |
require_receipt |
Every executed action must produce an immutable receipt. See Receipts & audit. |
If you deleted this whole file, the operator's proposals would all be refused. Policy grants permission; its absence never does. Richer policies, including value caps and per-action trust tiers, are covered in Governance & trust and Trust tiers.
Deploy and run once
Deploy the operator and its policy together, then run it a single time. --once executes one full sense, reason, dispose, act cycle and exits, which is the right way to watch a new operator before scheduling it.
fibric operators deploy ./operators/backlog-triage/operator.ts \
--policy ./operators/backlog-triage/policy.yaml
fibric operators run backlog-triage --once
Read the four lines back against the loop: the operator sensed through the Kustomer connector, reasoned and proposed a plan, the executor disposed of that plan against your policy, and only then did anything act on the real system.
Watch the first receipts arrive
Every action left a receipt. Tail them live, then open one as JSON to see the full record.
fibric receipts tail --operator backlog-triage
fibric receipts show rc_7c04 --json
{
"receipt_id": "rc_7c04",
"tenant_id": "t_8f2a…c901",
"operator": "backlog-triage",
"capability": "conversations.assign",
"proposed_by": "model",
"policy": { "decision": "allow", "rule": "conversations.assign" },
"idempotency_key": "backlog-triage:cnv-18841:assign",
"outcome": "applied",
"at": "2026-07-02T14:31:07Z"
}
The idempotency_key is the operator name, the entity, and the action: backlog-triage:cnv-18841:assign. If a retry, a duplicate event, or a second run proposes this same assignment again, the executor sees a key it has already recorded and returns a dedup instead of acting twice. This is the mechanism that makes a message flood structurally impossible, and it only works because the name you chose in step 2 is stable.
What you built
You now have a named, governed operator in production shape. Reading the pieces back:
- A connector binding: Kustomer bound to the role
support, exposingconversations.read,conversations.assign, andconversations.noteas capabilities. - An operator,
backlog-triage, with a plain-language goal and arun()that senses, reasons, and proposes, and cannot act. - A fail-closed policy that allows exactly two capabilities, caps each run, serializes side effects per conversation, and requires a receipt for every action.
- An audit trail: one immutable receipt per action, each carrying the policy decision and an idempotency key that guarantees the action never repeats.
To run it on a schedule instead of by hand, deploy without --once; the operator then runs on the trigger defined in its manifest. Scheduling and event triggers are covered in the guides.
Next steps
- Operators: the full anatomy and lifecycle of an operator.
- Trust tiers: graduate an operator from propose-only to autonomous within limits.
- Receipts & audit: querying, exporting, and retaining the audit trail.
- Guides: scheduling, event triggers, and production patterns.