Skip to content

TypeScript SDK

The TypeScript SDK (@fabric-platform/sdk) provides a type-safe client for the Fabric REST API with zero runtime dependencies.

Terminal window
npm install @fabric-platform/sdk

All imports come from the root package — no sub-path imports needed:

import { FabricClient, verifyWebhookSignature } from "@fabric-platform/sdk";
import type { DomainEvent, WorkflowRun, EventKind } from "@fabric-platform/sdk";
import { FabricClient } from "@fabric-platform/sdk";
// API key auth (server-side)
const fabric = new FabricClient({
baseUrl: "https://api.fabric.ai",
auth: { type: "api-key", key: "fab_your_key_here" },
organizationId: "org-uuid",
});
// Bearer token auth (client-side)
const fabric = new FabricClient({
auth: { type: "bearer", token: "jwt-token" },
});
// OAuth client credentials
const fabric = new FabricClient({
auth: { type: "oauth", clientId: "...", clientSecret: "..." },
});
// Dynamic token (Next.js server components)
const fabric = new FabricClient({
auth: () => getTokenFromCookies(),
});
// Consumer app auth (token exchange)
const fabric = new FabricClient({
app: {
clientId: process.env.FABRIC_CLIENT_ID!,
clientSecret: process.env.FABRIC_CLIENT_SECRET!,
},
});
// Act on behalf of a user:
const userFabric = await fabric.asUser(userId);
// Debug mode — logs all requests/responses
const fabric = new FabricClient({
debug: true,
onRequest: (method, url) => console.log(`${method} ${url}`),
onResponse: (method, url, status, ms) => console.log(`${status} (${ms}ms)`),
});
OptionTypeDescription
baseUrlstringFabric API URL (default: https://gofabric.dev)
authAuthConfigAPI key, bearer, OAuth, or dynamic token function
organizationIdstring?Default org ID for scoped requests
teamIdstring?Default team ID for scoped requests
timeoutnumberRequest timeout in ms (default: 30000)
fetchtypeof fetchCustom fetch (e.g., Next.js enhanced fetch)
debugbooleanLog all requests/responses to console
onRequestRequestInterceptorHook called before each request
onResponseResponseInterceptorHook called after each response
app{ clientId, clientSecret }Consumer app credentials for token exchange
// Sign up / log in
const auth = await fabric.auth.signup("user@example.com", "password");
const auth = await fabric.auth.login("user@example.com", "password");
// Token refresh
const newAuth = await fabric.auth.refresh(auth.refresh_token);
// Passwordless
await fabric.auth.magicLink("user@example.com");
await fabric.auth.forgotPassword("user@example.com");
await fabric.auth.resetPassword(accessToken, "new-password");
// Email verification
await fabric.auth.verify(verificationToken);
// Social login (returns redirect URL)
const url = fabric.auth.socialLoginUrl("google");
// MFA
const enrollment = await fabric.auth.mfa.enroll("totp", "My Phone");
const challenge = await fabric.auth.mfa.challenge(factorId);
const verified = await fabric.auth.mfa.verify(factorId, challengeId, "123456");
await fabric.auth.mfa.unenroll(factorId);
const me = await fabric.me.get();
const myOrgs = await fabric.me.organizations();
const myTeams = await fabric.me.teams();
const myPerms = await fabric.me.permissions();
// CRUD
const org = await fabric.organizations.create({ name: "Acme Corp" });
const orgs = await fabric.organizations.list();
const org = await fabric.organizations.get(orgId);
await fabric.organizations.update(orgId, { name: "New Name" });
await fabric.organizations.archive(orgId);
// Members & teams
const teams = await fabric.organizations.teams(orgId);
const members = await fabric.organizations.members(orgId);
const perms = await fabric.organizations.permissions(orgId);
// Settings
const settings = await fabric.organizations.getSettings(orgId);
await fabric.organizations.updateSettings(orgId, { display_name: "Acme Corp" });
// Usage & budget
const usage = await fabric.organizations.usage(orgId);
const daily = await fabric.organizations.usageDaily(orgId, { from: "2026-01-01", group_by: "provider" });
const records = await fabric.organizations.usageRecords(orgId);
const budget = await fabric.organizations.getBudget(orgId);
await fabric.organizations.setBudget(orgId, 500.0);
// Secrets (BYOK provider keys)
await fabric.organizations.createSecret(orgId, { name: "OPENAI_API_KEY", value: "sk-..." });
const secrets = await fabric.organizations.listSecrets(orgId);
await fabric.organizations.deleteSecret(orgId, "OPENAI_API_KEY");
// Audit & export
const logs = await fabric.organizations.auditLogs(orgId);
await fabric.organizations.export(orgId);
await fabric.organizations.requestDeletion(orgId);
// Register a workflow
const entry = await fabric.workflows.registry.create({
name: "video/ai_shorts",
language: "python",
source: "builtin",
});
// List & resolve
const workflows = await fabric.workflows.registry.list();
const entry = await fabric.workflows.registry.get("video/ai_shorts");
await fabric.workflows.registry.delete("video/ai_shorts");
// Compose workflows into a pipeline
const composed = await fabric.workflows.compose({
name: "research-to-video",
steps: ["research/deep_research", "video/ai_shorts"],
});
// Estimate cost before running
const estimate = await fabric.workflows.estimate("video/ai_shorts", {
input: { topic: "AI trends" },
});
// Submit a workflow run
const run = await fabric.workflows.runs.submit("video/ai_shorts", {
input: { topic: "AI trends", style: "professional" },
metadata: { requested_by: "campaign-123" },
priority: 90,
});
// List & get runs
const runs = await fabric.workflows.runs.list();
const run = await fabric.workflows.runs.get(runId);
// Control
await fabric.workflows.runs.cancel(runId, "no longer needed");
await fabric.workflows.runs.pause(runId);
await fabric.workflows.runs.resume(runId);
await fabric.workflows.runs.recover(runId);
// HITL approval
await fabric.workflows.runs.signal(runId, "approval", { approved: true });
await fabric.workflows.runs.approve(runId, { approved: true, reason: "looks good" });
const waiting = await fabric.workflows.runs.listWaiting();
// Node-level debugging
const attempts = await fabric.workflows.runs.getNodeAttempts(runId, "transcribe");

runs.getOutput(runId) returns a typed RunOutput with per-variant artifacts already grouped under each outputs[i].artifacts entry, with signed download_urls populated:

const result = await fabric.workflows.runs.getOutput(runId);
for (const variant of result.outputs) {
for (const a of variant.artifacts) {
console.log(a.filename, a.content_type, a.download_url, a.variant_index);
}
}

For “give me every file across every variant in one flat list”, use downloadAllArtifacts:

const downloads = await fabric.workflows.runs.downloadAllArtifacts(runId);
// → [{ artifact, filename, data: ArrayBuffer }, ...]

You can also list raw artifacts (no signed URLs) via artifactsWithUrls(runId) (which still works) or artifacts(runId).

// Submit and block until completion (with event streaming)
const completedRun = await fabric.workflows.runs.submitAndWait("video/ai_shorts", {
input: { topic: "AI trends" },
onEvent: (event) => console.log(event.kind, event.node_key),
timeoutMs: 600_000, // 10 minutes
});

submitAndGetOutput combines submit + wait + getOutput into one call. Returns a typed RunOutput whose outputs array always has one entry per variant (default outputs.length === 1):

const result = await fabric.workflows.runs.submitAndGetOutput("video/ai_shorts", {
input: { topic: "AI trends" },
});
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); // signed, no auth needed
}
}

Set variants (1–10) to fan out N parallel runs of the same workflow with the same input. Each gets its own subprocess and produces its own entry in outputs:

const result = await fabric.workflows.runs.submitAndGetOutput("video/ai_shorts", {
input: { topic: "AI trends" },
variants: 3,
});
result.outputs.length; // 3
result.outputs[0].variant_index; // 0
result.outputs[1].variant_index; // 1
result.outputs[2].variant_index; // 2

The SDK lifts the convenience kwarg into input.variants on the wire — variants is part of the workflow input contract, validated server-side (1–10).

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, so consumer UIs can route each entry to the right card layout / post template:

const result = await fabric.workflows.runs.submitBundle({
bundle: [
{ 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" } },
],
});
for (const variant of result.outputs) {
switch (variant.kind) {
case "video": /* render a video card */ break;
case "carousel": /* render a carousel card */ break;
case "image": /* render an image card */ break;
case "text": /* render a text card */ break;
}
}

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.

Regenerate — Create a New Run as a Variation of an Existing One

Section titled “Regenerate — Create a New Run as a Variation of an Existing One”

Pass regenerate on submit to mark the run as a regeneration of an earlier run/variant. The server persists parent_run_id + parent_variant_index as run lineage; workflows that care read direction / keep / extra_instructions off input.regenerate and modulate their prompts:

const result = await fabric.workflows.runs.submitAndGetOutput("video/ai_shorts", {
input: {
topic: "AI trends",
regenerate: {
direction: "punchier", // or deeper / contrarian / visual / data-first / surprise
keep: ["platform", "tone_of_voice"],
extra_instructions: "shorter, tighter hooks",
parent_run_id: priorRun.id,
parent_variant_index: 0,
},
},
});

direction is a prompt-level hint — workflows opt in to honor it. The lineage columns (parent_run_id, parent_variant_index) always persist regardless of which workflow you target; the prompt-modulating bits (direction, keep, extra_instructions) are read by stages via regenerateDirective(input.regenerate) and prepended to the relevant LLM/image prompt.

WorkflowWhat direction shifts
image/generateBoth the visual prompt and the overlay-text distillation — "punchier" yields a tighter hook + sharper composition, "contrarian" flips the angle, etc.

For workflows not on this list, direction is silently ignored — the run still executes and lineage still records, but the output is whatever the unmodulated prompt produces.

When you kick off a regen by replaying a previous run’s variant data, strip the output-shaped fields before submitting. Workflows declare their input contract narrowly (e.g. image/generate reads topic, hook, platform, thumbnail_text, thumbnail_style, num_thumbnails); fields like thumbnail_paths, thumbnail_asset_ids, selected_thumbnail, overlay_text, thumbnail_size, platform_exports, evaluation, video_asset_id are workflow outputs and should be filtered out of the replay. They survive validation as pass-through extras (so the run won’t fail), but they’re noise in storage and may surprise a future stage if it starts reading them.

A small stripFabricInternals(input) helper that drops the _fabric_* keys plus the per-workflow output keys is the cleanest place to centralize this.

Output Shape — Uniform Across Variants, Bundle, and Single

Section titled “Output Shape — Uniform Across Variants, Bundle, and Single”

runs.getOutput(runId) and runs.submitAndGetOutput(...) return a typed RunOutput:

interface RunOutput {
run_id: string;
status: string;
error: string | null;
variants: number; // count, equals outputs.length
outputs: VariantOutput[]; // one entry per variant; default 1
nodes: RunNodeOutput[];
}
interface VariantOutput {
variant_index: number;
workflow_name: string | null; // sub-workflow that produced this entry
kind: "video" | "carousel" | "image" | "text" | null;
output: unknown | null; // the workflow's terminal output
artifacts: RunArtifact[]; // produced by this variant only
}

Single-workflow runs have outputs.length === 1; you iterate the array uniformly — no shape branching.

The watch() method is the recommended way to observe workflow execution step-by-step:

const watcher = fabric.workflows.runs.watch(runId, {
// Optional: only watch specific nodes
nodeKeys: ["transcribe", "render", "compose"],
// Typed callbacks
onNodeStarted: (key, payload) => console.log(`${key} started`),
onNodeProgress: (key, payload) => console.log(`${key}: ${payload.percentage}%`),
onNodeCompleted: (key, payload) => console.log(`${key} done`),
onNodeFailed: (key, payload) => console.error(`${key} failed:`, payload),
onRunCompleted: (payload) => console.log("Workflow finished!"),
onRunFailed: (payload) => console.error("Workflow failed:", payload),
onRunCancelled: (payload) => console.log("Workflow cancelled"),
// Auto-reconnects on connection drop (default: true)
reconnect: true,
});
// Stop watching early
watcher.abort();
// Or wait until the run completes
await watcher.done;

Browser usage — direct streaming with bearer auth

Section titled “Browser usage — direct streaming with bearer auth”

watch() uses fetch + ReadableStream under the hood, not the native EventSource, so it works directly from the browser with custom Authorization headers. No proxy hop is required — your frontend can talk to the Fabric API directly.

The recommended auth pattern in the browser is a dynamic token provider that fetches a short-lived token from your own backend:

'use client';
import { FabricClient } from "@fabric-platform/sdk";
export const fabric = new FabricClient({
baseUrl: "https://api.fabric.example.com",
auth: async () => {
const res = await fetch("/api/fabric-token", { credentials: "include" });
const { token } = await res.json();
return token;
},
});

For React apps, the SDK ships a hook that wraps watch() and tracks node progress as typed state:

'use client';
import { useState } from "react";
import { useWorkflowRun } from "@fabric-platform/sdk/react";
import { fabric } from "./fabric-client";
export function RunMonitor({ workflowName }: { workflowName: string }) {
const [runId, setRunId] = useState<string | null>(null);
const run = useWorkflowRun(fabric, runId);
return (
<>
<button
disabled={runId !== null && !run.isComplete}
onClick={async () => {
const r = await fabric.workflows.runs.submit(workflowName, {
input: {},
});
setRunId(r.id);
}}
>
Run
</button>
<progress value={run.progressPercent} max={100} />
<p>{run.currentStep ?? (runId ? "waiting…" : "idle")}</p>
{run.isComplete && <p>Done: {run.runStatus}</p>}
</>
);
}

The hook cleans up the underlying stream on unmount, surfaces errors on run.error rather than throwing, and accepts runId: null as a no-op so you can render it before the run has been submitted.

CORS: the Fabric API must allow the caller’s origin, and its allow_headers list must include Authorization, Cache-Control, and Last-Event-ID. The default tower_http::cors config that ships with fabric serve already does.

See also: usePipelineProgress (same-origin proxy variant using EventSource) and the Streaming Events guide for the full picture.

For lower-level event streaming with filtering:

// Stream all events
const stream = fabric.events.stream({
onEvent: (event) => console.log(event.kind),
filter: { kinds: ["workflow.run.completed", "workflow.run.failed"] },
reconnect: true,
});
// Stream events for a specific run
const stream = fabric.events.streamRun(runId, {
onEvent: (event) => console.log(event.kind, event.node_key),
filter: { nodeKeys: ["transcribe", "render"] },
reconnect: true,
});
// Stream events for a specific job (single node)
const stream = fabric.events.streamJob(jobId, {
onEvent: (event) => console.log(event),
});
// Stop streaming
stream.abort();
await stream.done;
const ws = fabric.events.connectWebSocket({
onEvent: (event) => console.log(event),
onAck: (ack) => console.log("Server ack:", ack),
filter: { kinds: ["workflow.node.progress"] },
});
// Subscribe to a run dynamically
ws.subscribeToRun(runId);
// Unsubscribe
ws.unsubscribe();
// Close
ws.close();

Webhooks fire when a workflow run reaches a terminal state (completed/failed/cancelled). Use them for backend integrations and async pipelines.

// Create a webhook
const webhook = await fabric.webhooks.create(orgId, {
url: "https://your-app.com/webhook",
event_filter: ["workflow.run.completed", "workflow.run.failed"],
resource_filter: { workflow_definition_id: "..." },
max_retries: 5,
});
// List, update, delete
const webhooks = await fabric.webhooks.list(orgId);
await fabric.webhooks.update(webhookId, { active: false });
await fabric.webhooks.delete(webhookId);
// View delivery attempts
const deliveries = await fabric.webhooks.deliveries(webhookId);

Every webhook delivery is signed with HMAC-SHA256. Use constructWebhookEvent() to verify and parse in one call:

import { constructWebhookEvent } from "@fabric-platform/sdk";
app.post("/webhook", async (req, res) => {
let event;
try {
event = await constructWebhookEvent(
req.body, // raw body string
req.headers["x-fabric-signature"]!, // sha256=<hex>
process.env.WEBHOOK_SECRET!,
);
} catch {
return res.status(401).send("Invalid signature");
}
switch (event.kind) {
case "workflow.run.completed":
// Fetch output artifacts with download links
const artifacts = await fabric.workflows.runs.artifactsWithUrls(event.run_id!);
console.log("Output files:", artifacts.map(a => a.download_url));
break;
case "workflow.run.failed":
console.error("Run failed:", event.payload);
break;
}
res.sendStatus(200);
});

Or verify separately:

import { verifyWebhookSignature } from "@fabric-platform/sdk";
const valid = await verifyWebhookSignature(rawBody, signature, secret);

Use the built-in video/face-swap and video/motion-transfer workflows to create persona content:

import type { FaceSwapInput, MotionTransferInput } from "@fabric-platform/sdk";
// Face swap — swap a persona face onto a source image
const run = await fabric.workflows.runs.submitAndWait("video/face-swap", {
input: {
source_url: "https://example.com/dance-still.jpg",
target_url: "https://example.com/persona-face.png",
} satisfies FaceSwapInput,
});
console.log(run.output?.face_swap_url);
// Or pull the persona from an org gallery
const run = await fabric.workflows.runs.submitAndWait("video/face-swap", {
input: {
source_url: "https://example.com/dance-still.jpg",
persona_gallery_id: "gallery-uuid",
persona_index: 0,
} satisfies FaceSwapInput,
});
// Motion transfer — animate a persona with a reference video's motion
const run = await fabric.workflows.runs.submitAndWait("video/motion-transfer", {
input: {
source_image_url: "https://example.com/persona.png",
driving_video_url: "https://example.com/dance-reference.mp4",
} satisfies MotionTransferInput,
});
console.log(run.output?.motion_transfer_url);
// With Seedance for full-body motion
const run = await fabric.workflows.runs.submitAndWait("video/motion-transfer", {
input: {
driving_video_url: "https://example.com/choreography.mp4",
persona_gallery_id: "gallery-uuid",
motion_model: "seedance/2.0",
} satisfies MotionTransferInput,
});
// Download the result
const artifacts = await fabric.workflows.runs.artifactsWithUrls(run.id);
console.log(artifacts[0].download_url);

Execute AI providers directly with optional BYOK credentials:

// List available providers
const capabilities = await fabric.providers.list();
// Execute (request/response)
const result = await fabric.providers.execute({
modality: "text",
model: "gpt-4o",
input: { prompt: "Summarize this article..." },
credentials: { OPENAI_API_KEY: "sk-..." }, // BYOK (optional)
});
// Streaming execution (SSE)
const stream = fabric.providers.executeStream(
{ modality: "text", input: { prompt: "..." } },
{
onChunk: (chunk) => process.stdout.write(chunk.delta),
onComplete: (result) => console.log("\nDone:", result.usage),
},
);
// Cost estimation
const cost = await fabric.providers.estimate({ modality: "text", model: "gpt-4o", input: {} });
// Upload a file (max 100 MB)
const asset = await fabric.assets.upload(fileBlob, {
contentType: "video/mp4",
filename: "output.mp4",
});
// List assets
const assets = await fabric.assets.list();
// Generate a signed download URL
const { url } = await fabric.assets.signedUrl(assetId, 3600);
// Direct download (returns raw Response)
const response = await fabric.assets.download("path/to/file.mp4");
// Grant permission
await fabric.permissions.grant({
principal_id: userId,
organization_id: orgId,
role: "admin",
});
// Check single permission
const result = await fabric.permissions.check({
resource: `organization:${orgId}`,
action: "team.create",
});
// Batch check
const results = await fabric.permissions.checkBatch([
{ resource: `organization:${orgId}`, action: "read" },
{ resource: `team:${teamId}`, action: "invite" },
]);
// List & revoke
const perms = await fabric.permissions.list();
await fabric.permissions.revoke(permissionId);
// Create a cron schedule
const schedule = await fabric.schedules.create(workflowDefId, {
cron: "0 9 * * MON-FRI",
timezone: "America/New_York",
enabled: true,
});
// Manage schedules
const schedules = await fabric.schedules.list(workflowDefId);
await fabric.schedules.update(scheduleId, { enabled: false });
await fabric.schedules.delete(scheduleId);
// Manual trigger
await fabric.schedules.trigger(scheduleId);
const history = await fabric.schedules.history(scheduleId);
import { paginate, paginateAll, paginateItems } from "@fabric-platform/sdk";
// Page-level iteration
for await (const page of paginate((p) => fabric.organizations.list(p))) {
console.log(`Page with ${page.length} orgs`);
}
// Item-level iteration
for await (const org of paginateItems((p) => fabric.organizations.list(p))) {
console.log(org.name);
}
// Collect all into an array
const allOrgs = await paginateAll((p) => fabric.organizations.list(p));
import {
FabricError,
FabricAuthError,
FabricNotFoundError,
FabricRateLimitError,
} from "@fabric-platform/sdk";
try {
await fabric.organizations.get("bad-id");
} catch (err) {
if (err instanceof FabricNotFoundError) {
console.log("Not found:", err.message);
} else if (err instanceof FabricRateLimitError) {
console.log(`Rate limited, retry after ${err.retryAfterMs}ms`);
} else if (err instanceof FabricAuthError) {
console.log("Authentication failed");
} else if (err instanceof FabricError) {
console.log(`${err.code} (${err.status}): ${err.message}`);
console.log("Request ID:", err.requestId);
}
}

Register and manage consumer apps. See the App Registration guide for the full setup flow.

// Register a new app (returns client credentials — shown once)
const app = await fabric.apps.create({ name: "My App" });
console.log(app.client_id, app.client_secret);
// List your apps
const apps = await fabric.apps.list();
// Update settings
await fabric.apps.update(appId, {
auto_create_org: false,
allowed_origins: ["https://myapp.com"],
});
// Rotate client secret
const { client_secret } = await fabric.apps.rotateSecret(appId);
// Aggregate usage across all orgs in the app
const usage = await fabric.apps.usage(appId);
const fabric = new FabricClient({
app: { clientId: "...", clientSecret: "..." },
});
// Token exchange: get a user-scoped client
const userFabric = await fabric.asUser(userId);
// All operations scoped to this user's app-org
await userFabric.workflows.runs.submit("video/ai_shorts", { input: { ... } });
await userFabric.organizations.list(); // Only this app's orgs
// Bootstrap (dev mode)
await fabric.admin.bootstrap();
// System info
const info = await fabric.admin.systemInfo();
const status = await fabric.admin.systemStatus();
// Concurrency limits
const limits = await fabric.admin.listConcurrencyLimits();
await fabric.admin.setConcurrencyLimit("org:uuid", 10);
// Standalone audit logs
const logs = await fabric.admin.auditLogs();

All event kinds emitted by the platform:

CategoryEvent Kinds
Run lifecycleworkflow.run.created, queued, promoted, started, completed, failed, cancelled, waiting, paused
Node lifecycleworkflow.node.ready, claimed, started, progress, completed, failed, retried, skipped, cancelled, waiting_for_event, resumed
Job lifecyclejob.created, started, completed, failed
Scheduleworkflow.schedule.triggered, skipped, failed
Webhookwebhook.delivery.exhausted
Tenancyorganization.created, team.created, membership.created, invitation.created, accepted, revoked

The SDK provides 20 resource classes, all accessible as properties on FabricClient:

PropertyResource
fabric.authAuthentication (signup, login, MFA)
fabric.meCurrent user profile
fabric.organizationsOrganization CRUD, settings, usage, secrets
fabric.teamsTeam CRUD
fabric.workflowsRegistry, runs, composition, estimation
fabric.eventsSSE, WebSocket, event streaming
fabric.assetsUpload, download, signed URLs
fabric.galleriesGallery CRUD, items
fabric.apiKeysAPI key CRUD, rotate, disable
fabric.invitationsInvite, accept, revoke
fabric.permissionsGrant, check, batch check, revoke
fabric.webhooksWebhook CRUD, delivery history
fabric.serviceAccountsService account CRUD, API keys
fabric.oauthOAuth client management, token exchange
fabric.schedulesCron schedule CRUD, trigger, history
fabric.providersAI provider execution, streaming, BYOK
fabric.packagesInstalled package listing
fabric.adminSystem info, concurrency, bootstrap
fabric.appsConsumer app registration and management