Fibric. Docs fibric.io →
v0.9 ยท preview
Build

TypeScript SDK

@fibric/sdk is the typed client for the platform: publish envelopes, follow the stream, install and manage operators, inspect plans, and read receipts, all against the same public HTTP API every other client uses. It re-exports the kernel's types directly, so the EventEnvelope you publish and the PlannedAction you audit are the shapes documented in the event envelope and governance, with nothing lossy in between. This page is the working reference; the SDKs overview covers language support and roadmap.

Install

The client requires Node 18 or later. It is a different package from the authoring kit: install @fibric/sdk to call the platform, @fibric/connector-sdk to build things that plug into it. Most services need only one.

terminal
npm install @fibric/sdk

# a token to develop against; CI should use a workspace-scoped token instead
fibric auth login

Client initialization

The client takes a token, and the token decides everything else. A token resolves to exactly one tenant, so there is no tenant parameter to pass and no way to pass the wrong one; isolation is a property of the credential, not an argument your code gets right. Options can come from the constructor or the environment, and the constructor wins when both are set.

client init
import { Fibric } from '@fibric/sdk';

const fibric = new Fibric({
  token: process.env.FIBRIC_TOKEN!,     // or set FIBRIC_TOKEN and omit
  // baseUrl: 'https://api.fibric.io',  // FIBRIC_BASE_URL; point at fibric dev locally
  // timeoutMs: 30_000,                 // FIBRIC_TIMEOUT_MS
});

const me = await fibric.whoami();
// { tenant_id: 't_8f2a...', reseller_id: null, workspace: 'paperco-prod' }
OptionEnvironment variableDefaultDescription
tokenFIBRIC_TOKENRequired. Mint interactively with fibric auth login, or use a workspace-scoped CI token.
baseUrlFIBRIC_BASE_URLhttps://api.fibric.ioPoint at the local kernel during development.
timeoutMsFIBRIC_TIMEOUT_MS30000Per-request timeout. Retries on 429 and 5xx are built in with backoff.
!
The token is the blast radius

A FIBRIC_TOKEN can publish events into its tenant and read that tenant's receipts. Keep it out of source, rotate it like any production credential, and prefer short-lived CI tokens over long-lived personal ones.

Working with envelopes

Everything on the platform is an EventEnvelope, and the SDK's job around events is narrow: publish envelopes in, list and follow envelopes out. The type is re-exported from the kernel, field for field.

kernel types, re-exported
import type { EventEnvelope, ExecutionPlan, PlannedAction } from '@fibric/sdk';

// EventEnvelope
// {
//   event_id, reseller_id, tenant_id, workspace_id,
//   source, event_type, correlation_id, payload,
//   agent_id, session_id
// }

Publishing events

events.publish() takes the fields you own and returns the stamped envelope. The server assigns event_id, stamps reseller_id and tenant_id from the token, and generates a correlation_id if you did not supply one. Publishing never acts by itself: an event can only cause an operator to propose a plan, and the executor disposes that plan under policy.

publish
const env = await fibric.events.publish({
  source: 'warehouse-app',
  event_type: 'pick.exception',          // dotted noun.verb; operators match on this
  payload: { lane: 'B4', order_id: 'SO-11290' },
  // correlation_id: existing_id,        // pass one to join an existing thread
});

console.log(env.event_id, env.correlation_id);
FieldTypeRequiredDescription
sourcestringyesWhere this observation came from: "shopify", "bacnet-gw-7", "cron", "operator:jenny".
event_typestringyesDotted noun.verb type, for example order.created. The router matches operator triggers against it.
payloadRecord<string, unknown>noThe observation itself. Defaults to {}.
correlation_idstringnoJoins this event to an existing thread of work. Generated when omitted.
workspace_idstring | nullnoNarrows the event to one workspace within the tenant.
agent_id, session_idstring | nullnoSet when the event is an operator's own output; leave null for external observations.

Listing and filtering

Lists page like every other list in the SDK: an items array and an opaque cursor, plus an async iterator that drives the cursor for you. Filters combine as AND.

list envelopes
// one page
const page = await fibric.events.list({
  event_type: 'order.*',                // same glob grammar the router uses
  source: 'cn-magento',
  since: '2026-07-01T00:00:00Z',
  limit: 200,
});

// or iterate the whole window
for await (const env of fibric.events.iterate({ event_type: 'hvac.zone.*' })) {
  console.log(env.event_type, env.payload);
}

// one envelope by id
const one = await fibric.events.get('evt_01J9YV...');

Subscribing to the stream

events.subscribe() holds a streaming connection and yields envelopes as they arrive, resuming from the last delivered cursor on reconnect. Delivery is at-least-once; dedupe on event_id if your consumer is not naturally idempotent. Streaming delivery is a preview surface in v0.9: the shape below is stable, and cursor-polled iterate() is the fallback that works against every deployment today.

subscribe (preview)
const sub = fibric.events.subscribe({ event_type: 'order.*' });

for await (const env of sub) {
  await handle(env);                    // at-least-once: dedupe on env.event_id
}

// stop from another task
sub.close();
i
Subscribing is for observers, not operators

An operator does not subscribe with the SDK; it declares a trigger and the platform routes envelopes to it. subscribe() is for your own services: mirrors, dashboards, escalation hooks. See defining operators for the operator side.

Installing operators

operators.create() installs an operator, either from a marketplace pack by id or from a local definition you have pushed. Installing from a pack binds each declared capability to one of the tenant's connectors and applies the guardrail defaults you accept, exactly as the CLI flow does interactively. New installs default to propose-only mode: the operator senses and reasons, plans appear in the queue, and the executor disposes nothing until you flip the mode.

install from a pack
const op = await fibric.operators.create({
  from: 'op-order-sentinel',            // marketplace pack id
  name: 'order-risk',
  bindings: {                           // capability -> connection
    'commerce.orders': 'magento-live',
    'support.conversations': 'kustomer-live',
    'shipping.scans': 'ships-live',
  },
  acceptGuardrailDefaults: true,        // the pack's recommended TrustPolicy[]
  mode: 'propose-only',                 // default; 'live' is an explicit later step
});

// manage the lifecycle
await fibric.operators.pause(op.id);
await fibric.operators.resume(op.id);
const all = await fibric.operators.list();
FieldTypeRequiredDescription
fromstringyesPack id (op-order-sentinel) or a pushed local definition.
namestringyesThe operator's name within the tenant. Unique per workspace.
bindingsRecord<string, string>for packsMaps each capability the pack requires to a connection. Creation fails listing the unbound capabilities if incomplete.
acceptGuardrailDefaultsbooleannoApply the pack's recommended TrustPolicy[]. Defaults to false; the tenant policy always has the last word either way.
mode'propose-only' | 'live'noDefaults to propose-only. There is no mode that bypasses the trust policy.

Reading receipts

Every disposed action leaves a receipt, including refusals: a BLOCK is a receipt too, not an error you lost. The receipt carries the action as proposed, the executor's disposition, and the outcome, all joined to the originating envelope by correlation_id.

read receipts
// everything that happened because of one envelope
const receipts = await fibric.receipts.list({ correlation_id: env.correlation_id });

for (const r of receipts.items) {
  // decision: 'ALLOW' | 'BLOCK' | 'ALERT' | 'DEDUP'
  console.log(r.action.connector, r.action.tool, r.decision, r.ok);
}

// or stream a time window without managing cursors
for await (const r of fibric.receipts.iterate({ since: '2026-06-01' })) {
  if (r.decision === 'BLOCK') audit(r);
}
Receipt fieldTypeDescription
actionPlannedActionThe action exactly as proposed: connector, tool, args, value, entity_key, idempotency_key.
decision'ALLOW' | 'BLOCK' | 'ALERT' | 'DEDUP'The executor's disposition: the kernel's TrustDecision plus DEDUP for a side effect collapsed into an earlier identical one.
okbooleanWhether the handler succeeded. A BLOCK is ok: false with no handler call at all.
errorstring | undefinedThe failure or refusal reason when ok is false.
correlation_idstringJoins the receipt to its envelope, plan, and sibling actions.

Plans and dispositions

Between the envelope and the receipt sits the ExecutionPlan: what an operator proposed, before the executor disposed it. The SDK reads the plan queue, and for plans held for review, approves or vetoes them. This is the surface a human-in-the-loop tool builds on.

plan queue
// plans awaiting review (e.g. from operators in propose-only mode)
const pending = await fibric.plans.list({ status: 'pending' });

for (const plan of pending.items) {
  console.log(plan.reasoning);          // the operator's stated rationale
  for (const a of plan.actions) {
    console.log(' ', a.tool, JSON.stringify(a.args));
  }
}

await fibric.plans.approve(pending.items[0].id);
await fibric.plans.veto(pending.items[1].id, { reason: 'wrong order scope' });

Approval is consent, not command: an approved plan still passes the trust policy action by action, and an action no policy allows is still blocked. The actions & plans API documents the underlying endpoints.

Error handling

The client throws FibricError carrying the HTTP status, a stable error code, and the request id. Blocked actions are not thrown errors: the submission succeeded, the executor disposed fail-closed, and the dispositions are in the result. Inspect decision per action instead of wrapping submissions in try/catch and hoping.

error handling
import { FibricError } from '@fibric/sdk';

try {
  const result = await fibric.plans.submit(plan);
  for (const r of result.actions) {
    if (r.decision === 'BLOCK') console.warn('fail-closed:', r.action.tool, r.error);
  }
} catch (e) {
  if (e instanceof FibricError) {
    // e.status (HTTP), e.code (stable string), e.requestId (for support)
    if (e.status === 429) { /* backoff is built in; this is the ceiling */ }
  }
  throw e;
}

The full code table is on the errors page; throughput ceilings on rate limits & quotas.

Keep going