Skip to content

WASM SDK

The WASM SDK (@fabric-platform/wasm) is a WebAssembly build of the Rust SDK (fabric-platform on crates.io), published to npm. A single Rust source-of-truth compiled to two artifacts: a native rlib for Rust services, and a wasm-bindgen npm package for browsers, Node, Cloudflare Workers, Deno, and Bun.

Why WASM (and not just the TypeScript SDK)?

Section titled “Why WASM (and not just the TypeScript SDK)?”

The TypeScript SDK is the right choice for most browser apps — it’s smaller, faster to load, and exposes every endpoint. Reach for the WASM SDK when you want:

  • Type parity across Rust and JavaScript — request/response shapes are defined once, in Rust, and flow out to both sides. No drift.
  • Isomorphic Rust — your backend is Rust, and you’d rather ship the same client code into the browser than maintain a parallel hand-written JS wrapper.
  • Edge runtime portability — the same .wasm binary runs in browsers, Node ≥ 18, Cloudflare Workers, Deno, and Bun with no changes.
  • First-class SSE streaming — the WASM SDK uses fetch + ReadableStream for Server-Sent Events, which means it can stream authenticated endpoints (unlike browser EventSource, which refuses to attach Authorization headers).
Terminal window
npm install @fabric-platform/wasm
# or
pnpm add @fabric-platform/wasm
# or
bun add @fabric-platform/wasm

Requirements: any runtime that supports WebAssembly — modern browsers, Node ≥ 18, Cloudflare Workers, Deno, Bun.

import init, { FabricClient } from "@fabric-platform/wasm";
// Load the .wasm binary (only required in browsers; Node/bundlers skip this)
await init();
const client = new FabricClient("https://api.fabric.example", "sk-...");
client.setOrganizationId("org_123");
const me = await client.me();
console.log(me);

Run a workflow, stream its events, fetch the output

Section titled “Run a workflow, stream its events, fetch the output”

The SDK returns SSE streams as native ReadableStream<SseEvent>, so you can read them with a standard getReader() loop. When the run reaches a terminal state, getRunOutput(runId) gives you the workflow’s final output plus any artifacts it produced (each artifact comes with a signed download URL).

import init, { FabricClient } from "@fabric-platform/wasm";
await init();
const client = new FabricClient("https://api.fabric.example", "sk-...");
// 1. Kick off a workflow
const runId = await client.runWorkflow("research/problem-intelligence", {
niche: "elderly health",
});
// 2. Stream events as the run progresses
const stream = await client.streamWorkflowRun(runId);
const reader = stream.getReader();
try {
while (true) {
const { value, done } = await reader.read();
if (done) break;
console.log(value.event, value.data);
if (value.event === "run.completed" || value.event === "run.failed") break;
}
} finally {
reader.releaseLock();
}
// 3. Fetch the final output, artifacts, and per-task timeline
const result = await client.getRunOutput(runId);
console.log(result.status); // "completed"
console.log(result.variants); // 1 (or N for variant/bundle runs)
for (const variant of result.outputs) {
console.log(variant.workflow_name, variant.kind, variant.output);
for (const a of variant.artifacts) console.log("", a.filename, a.download_url);
}

runWorkflowWithVariants fans out N parallel runs (1–10) of the same workflow with the same input. The engine spawns one subprocess per variant; the unified outputs array on the run-output response has one entry per variant:

const runId = await client.runWorkflowWithVariants(
"video/ai_shorts",
{ topic: "AI trends" },
3,
);
// Wait for completion (poll, stream, or use the convenience wrapper):
const result = await client.runWorkflowAndGetOutput(
"video/ai_shorts",
{ topic: "AI trends", variants: 3 }, // variants can also live in input
3600, // signed URL TTL
);
result.outputs.length; // 3
result.outputs[0].variant_index; // 0

Bundle — N different workflows in parallel

Section titled “Bundle — N different workflows in parallel”

A bundle runs N independent sub-workflows in parallel from a single submission. Each entry’s output is tagged with its sub-workflow_name and (when the workflow declared one) kind:

const runId = await client.runBundle([
{ workflow: "video/ai_shorts", input: { topic: "AI trends" } },
{ workflow: "image/generate-post", input: { topic: "AI trends", slides: 6 } },
{ workflow: "content/threads-post", input: { topic: "AI trends" } },
]);
// Then either stream events as above, or fetch the final shape:
const result = await client.getRunOutput(runId);
for (const variant of result.outputs) {
// variant.kind tells you which card layout to render
}

bundle and variants are mutually exclusive — bundle is “N different workflows”, variants is “N copies of one”. The server returns 400 if both are set.

Workflows that declare Pydantic types have their input/output schemas automatically discovered at server boot and stored in the registry. The SDK exposes them via getWorkflowSchemas:

const schemas = await client.getWorkflowSchemas("research/problem-intelligence");
console.log(schemas.input_schema); // JSON Schema for what the workflow expects
console.log(schemas.output_schema); // JSON Schema for what it returns
console.log(schemas.task_schemas); // per-task breakdown (array)
console.log(schemas.warnings); // e.g. "task X has no Pydantic types"

The demo’s input editor uses input_schema to auto-populate the JSON textarea with a stub matching the workflow’s expected shape. The output panel shows output_schema as a collapsible “expected shape” section.

Pass validate = true when submitting a run to have the server validate your input against the workflow’s input_schema before enqueuing:

// Will return 400 with per-field errors if validation fails
const runId = await client.runWorkflow("research/trends", input, true);

Workflows without a schema are unaffected — the validation step is a no-op.

The WASM facade currently wraps 21 of the ~60 methods on fabric_sdk::FabricClient. The streaming methods and core workflow APIs are all in:

  • System: health()
  • Identity: me()
  • Orgs: listOrgs(), createOrg(slug, name), setOrganizationId(id)
  • API keys: listApiKeys()
  • Workflows: listWorkflows(), upsertWorkflow(name, body), runWorkflow(id, context, validate?), runWorkflowWithVariants(id, context, variants), runBundle(entries), runWorkflowAndGetOutput(id, context, expiresInSecs?), getWorkflowSchemas(name)
  • Runs: getRun(id), getRunOutput(id, expiresInSecs?), cancelRun(id)
  • SSE streams: streamWorkflowRun(id, includeInternal?), streamJob(id, includeInternal?), streamEvents(includeInternal?), streamProviderExecute(body)

The remaining 42 methods (auth, teams, invitations, webhooks, schedules, usage, audit, secrets, providers, convenience workflows like face_swap/motion_transfer) are tracked in sdks/rust-wasm/MISSING_METHODS.md and wrapped incrementally. Wrapping a new method is mechanical — copy the pattern from runWorkflow in sdks/rust-wasm/src/lib.rs.

Until a method is wrapped, you can still call it from Rust code that depends on fabric-platform and is compiled to wasm yourself.

Under the hood:

  1. sdks/rust/ is the native Rust SDK (fabric-platform on crates.io). It uses reqwest, which natively targets wasm32-unknown-unknown via the browser fetch API.
  2. Two small patches gate tokio::time and std::time::Instant behind cfg(not(target_arch = "wasm32")) and swap in gloo-timers + web-time on wasm. Native behaviour is unchanged.
  3. sdks/rust-wasm/ is a thin #[wasm_bindgen] facade crate that depends on fabric-platform and exposes a JS-ergonomic API. SSE streams flow through wasm-streams as native JS ReadableStream values.
  4. CI builds three wasm-pack targets (web, nodejs, bundler) and publishes them as a single npm package with an exports map so every runtime loads the right flavour automatically.

Release builds with wasm-opt -Oz produce a .wasm binary in the low hundreds of kilobytes — orders of magnitude smaller than PGlite (which ships all of Postgres) and comparable to libSQL’s HTTP-only build. The SDK is a thin REST/SSE wrapper with no embedded database or runtime.