Fibric. Docs fibric.io →
v0.9 · preview
Get started

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.

6 steps ~25 minutes Needs the Fibric CLI One connected system
i
What an operator is

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.

1

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.

bash
# 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
$ fibric capabilities ls CAPABILITY CONNECTOR STATUS conversations.read kustomer ready conversations.note kustomer ready conversations.assign kustomer ready

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.

2

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.

bash
# scaffold operator.ts and policy.yaml in the current project
fibric operators init backlog-triage
$ fibric operators init backlog-triage created operators/backlog-triage/operator.ts created operators/backlog-triage/policy.yaml ✓ scaffolded name is baked into idempotency keys, e.g. backlog-triage:<entity>:<action>
!
Renaming resets deduplication

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.

3

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.

operator.ts
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.

4

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.

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:

RuleWhat 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.
i
BLOCK is the default, not a setting

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.

5

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.

bash
fibric operators deploy ./operators/backlog-triage/operator.ts \
  --policy ./operators/backlog-triage/policy.yaml

fibric operators run backlog-triage --once
$ fibric operators run backlog-triage --once sensed 14 open conversations breaching first-response reasoned proposed 14 assignments + 14 internal notes disposed policy allow · within limit (14/20) · single-flight ok acted 14 assigned, 14 notes written · 28 receipts

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.

6

Watch the first receipts arrive

Every action left a receipt. Tail them live, then open one as JSON to see the full record.

bash
fibric receipts tail --operator backlog-triage
$ fibric receipts tail --operator backlog-triage receipt rc_7c04 cnv-18841 conversations.assign applied policy=allow idem=ok receipt rc_7c05 cnv-18841 conversations.note applied policy=allow idem=ok receipt rc_7c06 cnv-18857 conversations.assign applied policy=allow idem=ok
bash
fibric receipts show rc_7c04 --json
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:

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