Skip to main content

Embedding FluidGrids

There are two ways to put FluidGrids inside another product:

  • The React SDK (@burdenoff/fluidgrids-fe-sdk): the full builder, workflow list and run pages, rendered in your own React app with your user's session. This page covers its theme, token-refresh and event APIs.
  • The iframe viewer at https://app.fluidgrids.ai/embed/workflow: a public, read-only view of a workflow for any web page. See The iframe viewer.

Both always show the Powered by FluidGrids mark. Theme tokens change colours, corners and fonts; they do not hide or replace the mark.

Install and mount​

npm install @burdenoff/fluidgrids-fe-sdk

Wrap the SDK pages in FluidGridsProvider. The theme, auth and onEvent props below are the embedding hooks; the same props exist on FluidGridsRoot and WorkflowBuilder.

import "@burdenoff/fluidgrids-fe-sdk/styles.css";
import {
FluidGridsProvider,
FluidGridsRoot,
type FluidGridsEventHandler,
type FluidGridsTheme,
} from "@burdenoff/fluidgrids-fe-sdk";

const theme: FluidGridsTheme = {
mode: "light",
colors: { primary: "#2563eb", bgCanvas: "#f8fafc" },
radius: { base: "10px" },
font: { body: "Inter, system-ui, sans-serif" },
};

const onEvent: FluidGridsEventHandler = (name, payload) => {
if (name === "workflow:saved") console.log("saved", payload.workflowKey);
if (name === "run:completed") console.log(payload.runId, payload.status);
};

export function Workflows({ session }: { session: MySession }) {
return (
<FluidGridsProvider
apiGatewayUrl="https://graphqlworkspaces.burdenoff.com/workspaces/graphql"
globalApiUrl="https://graphql.burdenoff.com/global/graphql"
authToken={session.accessToken}
workspaceToken={session.workspaceToken}
workspaceId={session.workspaceId}
theme={theme}
auth={{ onTokenExpired: () => session.refresh() }}
onEvent={onEvent}
>
{/* SDK pages */}
</FluidGridsProvider>
);
}

If your app configures the SDK imperatively instead of through the provider, pass the same keys to setFluidGridsConfig({ theme, auth, onEvent }).

FluidGridsRoot, WorkflowBuilder and WorkflowBuilderCore do not need a theme provider from your app. When your page already has the Burdenoff (@burdenoff/fe-libs) ThemeProvider above them, they use it; when it has none, they mount their own, so there is never a second one.

Theme​

theme takes one of:

  • "light" or "dark": force that mode and keep the default colours.
  • An object with any of mode, colors, radius and font. Leave mode out to keep the mode your app (or the visitor) already uses.

Each token sets one or more CSS custom properties. The SDK and its dialogs are styled from these properties, so a token restyles everything that uses it.

TokenValueCSS custom properties
colors.bgCanvascolour--color-bg-canvas
colors.bgSurfacecolour--color-bg-surface
colors.bgElevatedcolour--color-bg-elevated
colors.bgSunkencolour--color-bg-sunken
colors.textPrimarycolour--color-text-primary
colors.textSecondarycolour--color-text-secondary
colors.textMutedcolour--color-text-muted
colors.borderDefaultcolour--color-border-default
colors.borderSubtlecolour--color-border-subtle
colors.borderStrongcolour--color-border-strong
colors.primarycolour--color-action-primary-bg, --color-action-primary-border
colors.primaryHovercolour--color-action-primary-bgHover
colors.primaryActivecolour--color-action-primary-bgActive
colors.primaryTextcolour--color-action-primary-text
colors.successcolour--color-status-success-bg
colors.warningcolour--color-status-warning-bg
colors.errorcolour--color-status-error-bg
colors.infocolour--color-status-info-bg
radius.baselength--radius
radius.cardlength--radius-card
radius.buttonlength--radius-button
radius.inputlength--radius-input
font.bodyfont list--font-family-body, --font-sans
font.headingfont list--font-family-heading
font.monofont list--font-family-mono, --font-mono

The same table is exported as FLUIDGRIDS_THEME_TOKEN_MAP, so you can build a settings screen from it.

How values are handled:

  • Colours accept hex (#2563eb), named colours, the CSS colour functions (rgb(), hsl(), oklch(), color-mix(), …) and var(--your-token). Lengths accept a number with a unit (8px, 0.5rem), 0, or calc(). Fonts accept a comma-separated family list; quote names with single quotes ('Fira Code', monospace).
  • A value that is not a plain CSS value (one containing ;, {, }, url(, expression(, comments or !important) is ignored and a warning is logged to the browser console. Unknown keys are ignored the same way.
  • The four radius tokens are independent: radius.base does not change --radius-card, --radius-button or --radius-input, which keep their defaults (12px, 8px, 8px). Builder node cards use radius.card. Set all four for one consistent corner radius.
  • When you set colors.primary alone, the hover and pressed shades are derived from it, and for a hex colour the text on it is set to black or white for contrast. Set primaryHover, primaryActive or primaryText to override them.

The theme is applied to the whole page, not to a wrapper element: the SDK renders in your document and its menus and dialogs render at the end of body, so the properties are set on the <html> element while the provider is mounted. Unmounting the provider, or changing the theme, puts back the values the page had before. If your own UI reads the same custom properties, it picks up the theme too.

Outside the provider you can apply a theme yourself:

import {
applyFluidGridsTheme,
themeToCssVariables,
useFluidGridsTheme,
} from "@burdenoff/fluidgrids-fe-sdk";

themeToCssVariables({ colors: { primary: "#16a34a" } }); // { "--color-action-primary-bg": "#16a34a", … }
const remove = applyFluidGridsTheme("dark"); // returns a function that removes it again
useFluidGridsTheme(theme); // in a component: applied while mounted

Token refresh​

Workspace tokens are short-lived, so a builder left open outlives the token it started with. Give the SDK a way to get a new one:

<FluidGridsProvider
// …
auth={{
onTokenExpired: async () => {
const next = await mySession.refresh();
return next.accessToken; // or { authToken, workspaceToken }
},
}}
/>

When a request fails because the session expired (HTTP 401, a GraphQL UNAUTHENTICATED error, or an expired or revoked credential), the SDK:

  1. calls onTokenExpired once. Requests that fail while it is running wait for the same call;
  2. stores the new token and retries the failed request once with it;
  3. if onTokenExpired rejects or returns an empty token, emits auth:failed and lets the request fail with its original error.

A request is never retried more than once, so a token that keeps being refused cannot cause a loop.

A failed refresh is reported once. onTokenExpired is called once and auth:failed is sent once for each failed refresh, however many requests were in flight with the expired token: a request sent before a refresh that has since failed fails with its original error without calling onTokenExpired again, even when its own 401 arrives after the failure. A request sent after the failure starts a new refresh, so once your app has a valid session again (for example, the user signed in again) the SDK picks it up. Return a string to replace the user token, or an object { authToken?, workspaceToken? } to replace either or both. Subscriptions send the current token whenever they reconnect.

Your own Apollo client​

If you pass your own Apollo clients (workspaceClient / globalClient), add the refresh link to their chain, before the link that sets authorization:

import { ApolloLink, HttpLink } from "@apollo/client";
import { setContext } from "@apollo/client/link/context";
import {
createFluidGridsTokenRefreshLink,
getFluidGridsConfig,
} from "@burdenoff/fluidgrids-fe-sdk";

const authLink = setContext((_, { headers }) => ({
headers: {
...headers,
authorization: `Bearer ${getFluidGridsConfig().authToken}`,
},
}));

const link = ApolloLink.from([
createFluidGridsTokenRefreshLink(),
authLink,
new HttpLink({ uri: "https://graphqlworkspaces.burdenoff.com/workspaces/graphql" }),
]);

The link reads auth.onTokenExpired from the SDK config and stores the new token there. If your client keeps its token elsewhere, pass your own callback and setter: createFluidGridsTokenRefreshLink({ onTokenExpired, onTokenRefreshed }).

Events​

onEvent(name, payload) is called for each of these events. Checking name narrows the type of payload in TypeScript.

EventPayloadWhen
workflow:saved{ workflowKey, version?, id? }A save reached the server: a new workflow, an update, or a new version.
workflow:activated{ workflowKey, version? }A version was activated.
run:started{ runId, workflowKey?, mode? }The server accepted a run: a builder test run, a run from a node, a manual trigger, a replay or a retry.
run:completed{ runId, status, workflowKey? }A run the builder is following reached COMPLETED, FAILED, CANCELLED or TERMINATED. Sent once per run.
node:selected{ nodeId, nodeType? }Exactly one node became selected on the canvas. nodeId: null means the selection was cleared. Selecting several nodes sends nothing.
error{ source, message, code? }A save (workflow:save), an activation (workflow:activate) or a run start (run:start) failed. message is fit to show; code is the API error code when there is one.
auth:failed{ message }The session expired and onTokenExpired could not provide a new token.

Events are delivered synchronously. If your handler throws, the error is logged and the SDK carries on.

The standalone builder​

WorkflowBuilder and WorkflowBuilderCore are a lightweight canvas that works on the workflow you pass in. Its Save and Run buttons do not call the FluidGrids API: Save passes the edited workflow to your onSave, and Run passes a client-generated id (run-<timestamp>) to your onExecute. Saving and starting the run are your app's job, so this canvas sends only node:selected, with the same rules as the full builder. It never sends workflow:saved, run:started or error. FluidGridsRoot and the full builder page send every event in the table above.

Versioning​

The SDK exports the contract version as FLUIDGRIDS_EVENTS_VERSION (currently 1.0.0) and the event names as FLUIDGRIDS_EVENT_NAMES. The version follows semantic versioning:

  • Minor: a new event, or a new optional field in a payload. Ignore names you do not know.
  • Major: an event removed or renamed, a field removed, or a field's type or meaning changed. A major change is released as a breaking SDK release.

The iframe viewer​

https://app.fluidgrids.ai/embed/workflow renders a workflow read-only, without signing in. The app's Share dialog does not produce an embed snippet; build the <iframe> yourself as shown below. The viewer reads these query parameters:

ParameterValueEffect
productIdstore product idThe workflow template to show. Without it, the viewer waits for a postMessage from the parent page.
themelight or darkForces the mode. Without it, the visitor's own preference is used.
accentcolourPrimary colour (colors.primary). A bare hex value works without #: accent=2563eb.
radiuslengthCorner radius for the base, cards (including the node cards), buttons and inputs: it sets radius.base, radius.card, radius.button and radius.input to the same value. For example 12px.
fontfont listBody font (font.body), for example Inter,sans-serif.
ctafalseHides the Use this template button.

accent, radius and font go through the same checks as an SDK theme; a value that fails them is ignored. For example:

<iframe
src="https://app.fluidgrids.ai/embed/workflow?productId=YOUR_PRODUCT_ID&theme=dark&accent=2563eb&radius=12px&font=Inter,sans-serif"
width="100%"
height="520"
style="border: 0"
title="FluidGrids workflow"
></iframe>

Sending a workflow with postMessage​

Without productId, the viewer shows a workflow your page sends it. When it is ready it posts { type: "fluidgrids:embed:ready" } to the parent page. Reply with:

iframe.contentWindow.postMessage(
{
type: "fluidgrids:embed:openWorkflow",
workflow: { name: "My workflow", nodes: [/* … */], edges: [/* … */] },
},
"https://app.fluidgrids.ai",
);

The viewer then posts { type: "fluidgrids:embed:loaded", nodeCount }, or { type: "fluidgrids:embed:error", message } when the message has no nodes or a productId fails to load. Each openWorkflow message replaces the workflow on show.