Skip to main content

SDK Reference

The @facilio/vibe-sdk is the browser-side library that handles auth and data access for Vibe apps.

Install#

Bundler (Vite / webpack / Next.js / esbuild / Astro / …)#

npm install @facilio/vibe-sdk
import { createVibe } from '@facilio/vibe-sdk';
const vibe = createVibe();   // serverURL defaults to window.location.origin

Because every deployed Vibe app is served from https://<linkName>.vibe.facilio.com, the SDK talks to the same origin and cookies flow automatically — no serverURL override needed.

Plain HTML (no bundler)#

Load from CDN — no npm install required:

<script src="https://unpkg.com/@facilio/vibe-sdk"></script><script>  const vibe = VibeSDK.createVibe();</script>

createVibe(options?)#

Create the SDK client.

const vibe = createVibe({ serverURL: window.location.origin });
OptionDefaultNotes
serverURLwindow.location.originOverride only if you're testing against a different host.

Returns a Vibe instance with the methods below.


Auth#

getCurrentUser()#

The single source of truth for "is the user signed in?".

const me = await vibe.getCurrentUser();

Returns (signed in):

{  "user": {    "uid": 1,    "email": "xyz@facilio.com",    "name": "xxxxx",    "username": "xyz"  },  "org": {    "orgId": 1  }}

Returns null when not signed in (the underlying /api/runtime/getCurrentUser returned 401).

Access fields under the nested shapeme.user.email, me.user.name, me.user.uid, me.user.username, me.org.orgId. There is no me.email at the top level.

isAuthenticated()#

Boolean check.

const ok = await vibe.isAuthenticated();

login()#

Redirect the browser to identity-service. Synchronous in effect — nothing after this line runs because the browser navigates away.

vibe.login();

Use this only when getCurrentUser() returned null. Do not wire it into the catch block of every action call — a 401 from executeAction is just an error.

logout()#

Sign out the current session.

vibe.logout();

Recommended bootstrap pattern#

Run on app mount. This is the canonical way to gate an app behind auth:

const me = await vibe.getCurrentUser();if (!me) {  vibe.login();   // browser navigates away; nothing below runs  return;}// ...render authenticated UI using me.user.email, me.org.orgId, etc.

Or, if you prefer an explicit "Log in" button:

const me = await vibe.getCurrentUser();if (!me) {  // render <button onClick={() => vibe.login()}>Log in</button>  return;}

Either pattern is fine — just never leave a null user case unhandled.


Data — executeAction#

The only sanctioned way to access Facilio data from a Vibe app.

const result = await vibe.executeAction(connectionSlug, actionSlug, payload);
ArgumentNotes
connectionSlugIdentifies which Facilio product/connector to talk to (e.g. facilio-cmms).
actionSlugIdentifies the specific operation (e.g. list-assets, create-workorder).
payloadJSON object with the action's inputs. Defaults to {} if the action takes none.

Returns the parsed JSON response. The shape is action-specific — the SDK does not normalize it. A typical action returns { response: { data: [...] } }, but always confirm by inspecting the action's output schema via facilio connections schemas <slug> --with-output.

Where do connectionSlug, actionSlug, and payload come from?#

Not from your imagination — from the Facilio CLI. @facilio/cli ships facilio connections — the same authenticated session as facilio login gives you discovery and execution over every available connection and action.

Workflow when you need to call data:

# 1. Find the action by describing what you want to do.facilio connections search "list assets"facilio connections search "create workorder" --app facilio-cmms
# 2. Read the input schema so you know the payload keys — add --with-output for the response shape.facilio connections schemas facilio-cmms.list-assets --with-output
# 3. Dry-run against the real server to validate your payload before wiring it into code.facilio connections execute facilio-cmms.list-assets --params '{}' --dry-run
# 4. Execute for real — same code path the SDK takes at runtime. If this returns data, the SDK call will too.facilio connections execute facilio-cmms.list-assets --params '{}'

Then pass the same <connection>, <action>, and payload shape to vibe.executeAction(connectionSlug, actionSlug, payload) from your app. Full CLI surface: CLI Reference › Connections.

Do not hardcode action lists from documentation — they evolve. Rediscover via facilio connections search each time you add new functionality.

Reference call#

const result = await vibe.executeAction('facilio-cmms', 'list-assets');const { response } = result;const assets = response?.data ?? [];

With a payload#

const { response } = await vibe.executeAction(  'facilio-cmms',  'create-workorder',  {    subject: 'Replace HVAC filter',    siteId: 123,    dueDate: new Date('2026-07-01').getTime(),  // milliseconds since epoch  });

Functions — executeFunction#

Execute a handler of one of your app's built functions. Functions are authored, compiled to WASM, and deployed with the facilio vibe function CLI; they run server-side and can query the app's database and call Facilio connections.

const result = await vibe.executeFunction(name, handler, args);
ArgumentNotes
nameThe function's logical name (e.g. workorderlist).
handlerThe handler registered via server.addHandler({ name }) in the function's code.
argsJSON object of handler arguments. Defaults to {}.

Returns the handler's output (its return value). Throws VibeError if the request fails or the function itself errors.

const orders = await vibe.executeFunction('workorderlist', 'list', { limit: 20 });

App scoping and secrets are automatic. The backend resolves which app you're in from the request origin (the app's subdomain), so a deployed app can only run its own functions — you never pass an app id. The secrets a function needs (connections/agents tokens, and the app's DB schema/user) are injected server-side; the browser sends only args.

Background runs — executeFunctionAsync#

Starts a run and returns as soon as the server accepts it, rather than holding the connection open for the whole handler. Reach for it when the work outlives the wait a user will tolerate — building a report, sending a batch of emails, a long sync. When the UI needs the result now, executeFunction is still the right call.

const { runId } = await vibe.executeFunctionAsync(name, handler, args, opts);
ArgumentNotes
nameThe function's logical name.
handlerThe handler registered via server.addHandler({ name }).
argsJSON object of handler arguments. Defaults to {}.
opts.timeoutSecondsWall-clock ceiling for the run, 1900. Defaults to 900.

Resolves to { runId, name, handler, accepted } — typically within milliseconds. The run executes as the same caller and on the same deployment channel a synchronous call would use, so the only difference a handler can observe is that nobody is waiting for it.

A resolved promise means accepted, not succeeded. Nothing about the run is stored, there is no status endpoint, and the platform publishes no event of its own. Three consequences follow, and all three are the app's responsibility:

  1. Your handler must report its own outcome. Wrap the body in try/catch and publish both branches with VibeEvents, then subscribe in the browser. Without it, a failure exists only in server logs.
  2. Correlate with an id you control. The returned runId is not visible inside the function. Pass your own id in args and echo it in the events you publish.
  3. Long-running UI needs its own timeout. A run cancelled at its timeoutSeconds ceiling, or killed by a deployment, publishes nothing — and looks exactly like a run still in progress.

Putting it together — a monthly report that takes too long to wait for:

// browserconst myId = crypto.randomUUID();
// Subscribe FIRST. A fast run can finish before a late subscribe lands, and// there is no replay.vibe.subscribe('reports', (evt) => {  if (evt.payload.runId !== myId) return;  if (evt.payload.ok) showLink(evt.payload.url);  else showError(evt.payload.error);});
await vibe.executeFunctionAsync('reports', 'monthly', { runId: myId, month: '2026-01' });// resolves immediately — the report has not been built yet
// functions/reports.ts — publish BOTH branches, or the browser waits foreverserver.addHandler({  name: 'monthly',  description: 'Build the monthly report and announce it',  parameters: {    runId: { description: 'Caller correlation id', type: 'string' },    month: { description: 'YYYY-MM', type: 'string' },  },  execute: async (args) => {    try {      const url = await buildReport(args.month);      await events.publish('reports', { runId: args.runId, ok: true, url });      return { ok: true, url };    } catch (e) {      await events.publish('reports', { runId: args.runId, ok: false, error: e.message });      throw e;    }  },});

Throws VibeError with status 429 when the app already has too many runs in flight (backpressure — wait for some to finish), and 400 when the function is not built. Accepted runs are held in memory by the server that accepted them and do not survive a restart, so work that must not be lost belongs in a scheduled job, which is durable by design.


Agents — executeAgent#

Invoke one of your app's LLM agents. Agents are authored and configured with the facilio vibe agent CLI. If the agent was created with --stateful, multi-turn conversations for the signed-in user carry over across calls, page reloads, and devices — no thread id to manage from the client.

const result = await vibe.executeAgent<AgentResponse>(name, input, options?);
ArgumentNotes
nameThe agent's logical name (e.g. hello-agent, sentiment-agent) — same name you used at facilio vibe agent create.
inputPlain-string prompt for the model.
options.fileIdsOptional number[] of ids from uploadFile to attach to this run. Requires @facilio/vibe-sdk v0.2.1+. See attachments.

Returns the run result. Throws VibeError if the request fails or the run errors.

You never pass a thread id, and you never pass an app id. The server resolves the app from the request host (the subdomain the browser is on) and — for --stateful agents — resolves or creates the thread for the signed-in user.

Response shape#

{  "status": "completed",  "response": {    "content": "Hello, Vishnu!",    // string OR stringified JSON — see below    "thread_id": 31541,    "id": 123682,    "role": "system"  },  "run_id": 41788,  "thread_id": 31541,  "immediateResponse": true,  "backgroundResponse": false,  "error_message": null}

For free-form agents, response.content is the model's plain-text reply — render it directly.

Structured output — parse response.content#

If the agent was created with an --output-schema, the model's reply is validated against the schema and returned as a JSON string in response.content. Parse it before use:

const res = await vibe.executeAgent('sentiment-agent', text);const raw = res.response?.content;const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;// parsed → { sentiment: 'positive', confidence: 0.95, keywords: [...], summary: '...' }

The schema (including your title, required, enum / items, additionalProperties: false) is what the runtime enforces on the provider — the shape you get back matches what you registered. Confirm the exact schema with facilio vibe agent get <name>.

Stateful agents — automatic per-user memory#

For agents created with --stateful, a second call from the same signed-in user continues the first — no thread id to pass, and memory survives page reloads and device switches:

// Turn 1await vibe.executeAgent('hello-agent-stateful', 'Hi, my name is Vishnu.');
// Later — after a reload, or on another device signed in as the same userconst res = await vibe.executeAgent('hello-agent-stateful', "What's my name?");console.log(res.response.content);   // "Your name is Vishnu."

Stateless agents get a fresh conversation on every call.

Reference call#

const res = await vibe.executeAgent('hello-agent', 'Say hi to Vishnu');console.log(res.response.content);   // "Hello, Vishnu!"

With structured output#

const res = await vibe.executeAgent('sentiment-agent', reviewText);const parsed = JSON.parse(res.response.content);render({  sentiment: parsed.sentiment,       // 'positive' | 'negative' | 'neutral'  confidence: parsed.confidence,     // number in [0, 1]  keywords: parsed.keywords,         // string[]  summary: parsed.summary,           // string});

Attachments — send files to an agent#

Upload the file to the app's file store first, then pass its id:

const photo = await vibe.uploadFile(file);const res = await vibe.executeAgent('inspector', 'What is wrong here?', {  fileIds: [photo.fileId],});

How the server handles them:

  • image/* files go to the agent's vision input; every other content type is extracted into the agent's context as text.
  • A single run is capped at 10 files.
  • Attachments belong to the run they're sent with. For a stateful agent, the thread already remembers them — do not re-send the same fileIds on the next turn.
  • fileIds must belong to this app's file store. Omitting the option entirely produces a request byte-identical to a call with no attachments.
Uploads reach the model provider

Anything a user attaches is sent to the agent's configured provider. Say so in the UI when your app accepts documents or photos.


Files — the app file store#

Every Vibe app has a private file store, available from the browser in @facilio/vibe-sdk v0.2.1+. It is a runtime-only surface — there is no facilio vibe files CLI command and no author-time upload path.

The unit of currency is the fileId: a number returned on upload that is the durable handle for everything downstream — rendering the file back, attaching it to an agent, or storing it beside your own records. The underlying storage path is never exposed to the browser.

const stored = await vibe.uploadFile(file);   // → { fileId: 4821, fileName: 'photo.png', … }

Three properties shape everything below:

  • App scope comes from the request host. You never pass an app id, and one app cannot read another's files.
  • fileId is the only handle. Lose the id and, as far as your app is concerned, you've lost the file.
  • Uploads are append-only. Two uploads of photo.png are two files with two ids — no overwrite, no dedup.

VibeFile#

interface VibeFile {  fileId: number;               // the durable handle — this is what you keep  fileName: string;  contentType: string | null;  size: number | null;          // bytes  uploadedTime: number | null;  // epoch ms  uploadedBy: number | null;    // user id}

uploadFile(file, name?)#

const stored = await vibe.uploadFile(file);                    // File from an <input>const stored = await vibe.uploadFile(blob, 'signature.png');   // Blob needs a name
ArgumentTypeNotes
fileFile \| BlobRequired.
namestringOverrides the filename recorded server-side. Effectively required for a Blob — a canvas export, pasted screenshot, or generated PDF has no name of its own.
Don't set Content-Type yourself

The SDK posts multipart/form-data and deliberately lets the browser set the header, because only the browser knows the multipart boundary it generated. Hand-setting the header produces a request the server can't parse — this is the classic failure when someone bypasses the SDK.

listFiles()#

const files: VibeFile[] = await vibe.listFiles();   // this app's files, newest first

Good for a "recent uploads" panel. It is not a substitute for storing ids next to your own records — the store has no notion of which inspection or work order a file belongs to. That link is yours to keep.

downloadFile(fileId)#

const blob = await vibe.downloadFile(4821);const url = URL.createObjectURL(blob);img.src = url;// …on unmount:URL.revokeObjectURL(url);

For a file the user just picked, skip the round trip and use URL.createObjectURL(file) on the local File. downloadFile is for rendering something you only hold an id for — after a reload, on another device, or for another user.

deleteFile(fileId)#

await vibe.deleteFile(4821);

Soft delete — reads stop serving the file immediately. Worth calling when a user removes an attachment before submitting, so the upload isn't left orphaned.

Reference pattern — pick, preview, keep the id#

const MAX_BYTES = 10 * 1024 * 1024;   // a limit your app chooses, not a platform cap
async function onPick(e: React.ChangeEvent<HTMLInputElement>) {  const file = e.target.files?.[0];  e.target.value = '';                         // let the same file be re-picked  if (!file) return;  if (!file.type.startsWith('image/')) return setError('Images only.');  if (file.size > MAX_BYTES) return setError('Max 10 MB.');
  const stored = await vibe.uploadFile(file);  setPreview(URL.createObjectURL(file));       // local preview — no round trip  setFileId(stored.fileId);                    // persist this somewhere durable}

Then persist the id where the record lives — an app-database column, a function payload, or a connection action — so you can read it back and re-render after a reload:

const photo = await vibe.uploadFile(file);await vibe.executeFunction('inspections', 'addPhoto', {  inspectionId,  fileId: photo.fileId,  fileName: photo.fileName,});
A fileId is a handle, not an authorization

If a file belongs to a specific record, store that link yourself and check it before showing the file to another user. The file store enforces app scope, not your app's row-level rules.


Realtime — subscribe#

Receive live updates over a WebSocket instead of polling. Requires @facilio/vibe-sdk 0.3.0+.

const sub = vibe.subscribe<{ type: string; post: Post }>('posts', (evt) => {  if (evt.payload.type === 'post.created') prependCard(evt.payload.post);});
sub.unsubscribe();

Events are published by the app's own server functions (events.publish(topic, payload) from @facilio/studio-functions) — the browser can subscribe but never publish.

Each callback receives a VibeEvent<T>:

interface VibeEvent<T = unknown> {  topic: string;    // the topic you subscribed with  eventId: string;  // unique id for this event  ts: number;       // publish time, epoch ms  payload: T;       // exactly what the function published}
MethodPurpose
subscribe(topic, handler)Start receiving events on topic. Returns { unsubscribe() }.
realtimeState'idle' / 'connecting' / 'open' / 'reconnecting' / 'closed'.
onRealtimeState(listener)Notified on state change — for a "Live" indicator. Returns a function that removes the listener.
closeRealtime()Close the connection and drop every subscription.
vibe.realtimeState;const stop = vibe.onRealtimeState((state) => setLive(state === 'open'));vibe.closeRealtime();

Reconnection and re-subscription are automatic. Events published while a tab is disconnected are not replayed, so reload your data when the state becomes 'open'.

See Realtime for the server-side publish call and topic naming.


Raw HTTP — fetch(path, init?)#

Escape hatch for endpoints that aren't exposed as actions. A thin wrapper around fetch() that:

  • Attaches credentials: 'include' so the session cookie flows.
  • Redirects to login() automatically on 401.
const res = await vibe.fetch('/api/runtime/...');const json = await res.json();

Prefer executeAction whenever possible — it's the supported, schema-discoverable surface.


Error handling#

All SDK methods throw VibeError. The error has:

  • .message — human-readable string.
  • .status? — HTTP status if available.
try {  const result = await vibe.executeAction('facilio-cmms', 'list-assets');  // ...use result} catch (err) {  showError(err.message);   // do NOT call vibe.login() here  console.error('status:', err.status);}

A 401 from executeAction is not a signal to call vibe.login(). The login redirect belongs only on the getCurrentUser() path. If executeAction returns 401, surface the message — the session was likely revoked, and the next mount cycle will trigger the login flow through getCurrentUser().


Method index#

MethodPurpose
createVibe(options?)Create the SDK client.
getCurrentUser()Returns { user, org } or null. The single source of truth for "signed in?".
isAuthenticated()Boolean check.
login()Redirect to identity-service. Use only when getCurrentUser() returns null.
logout()Sign out the current session.
executeAction(connectionSlug, actionSlug, payload)The only sanctioned way to call Facilio data. Discover slugs via facilio connections search.
executeFunction(name, handler, args?)Run one of the app's built server-side functions.
executeFunctionAsync(name, handler, args?, opts?)Start a run in the background and get a runId immediately. The handler reports its own outcome over realtime.
executeAgent(name, input, opts?)Invoke one of the app's LLM agents. Stateful agents auto-scope a per-user thread; opts.fileIds attaches uploaded files.
uploadFile(file, name?)Upload to the app's file store. Returns a VibeFile whose fileId is the durable handle.
listFiles()The app's stored files, newest first.
downloadFile(fileId)Fetch a stored file's bytes as a Blob.
deleteFile(fileId)Soft-delete a stored file.
subscribe(topic, handler)Receive live events on a topic over one shared WebSocket. Returns { unsubscribe() }.
realtimeStateCurrent transport state: idle / connecting / open / reconnecting / closed.
onRealtimeState(listener)Observe transport state. Returns a function that removes the listener.
closeRealtime()Close the realtime socket and drop every subscription.
fetch(path, init?)Raw HTTP escape hatch with credentials: 'include' and auto-redirect on 401.

End-to-end example#

import { useEffect, useState } from 'react';import { createVibe } from '@facilio/vibe-sdk';
const vibe = createVibe();
export default function App() {  const [me, setMe] = useState(null);  const [assets, setAssets] = useState([]);  const [error, setError] = useState(null);
  useEffect(() => {    (async () => {      const user = await vibe.getCurrentUser();      if (!user) {        vibe.login();        return;      }      setMe(user);      try {        const { response } = await vibe.executeAction('facilio-cmms', 'list-assets');        setAssets(response?.data ?? []);      } catch (err) {        setError(err.message);      }    })();  }, []);
  if (error) return <p>{error}</p>;  if (!me) return <p>Loading…</p>;
  return (    <main>      <h1>Hello, {me.user.name}</h1>      <p>Org: {me.org.orgId}</p>      <button onClick={() => vibe.logout()}>Log out</button>      <h2>Assets ({assets.length})</h2>      <ul>        {assets.map(a => <li key={a.id}>{a.name}</li>)}      </ul>    </main>  );}

See Getting Started for the full project setup and Building with AI Agents for how to hand this off to an AI agent.