What ships to the browser
A workflow file holds everything: every role's stages and every action's settings, including AI prompts, schemas, webhook addresses and email text. The engine needs all of it. A browser doesn't.
Your bundler doesn't know that. Import the file as it is and every visitor downloads the whole thing: the manager's review form, the underwriter's prompt, the address your webhook listens on.
The Clipless bundler plugin strips what a browser doesn't need, as the file is compiled.
What it protects
The engine never trusts the browser. Another role's answers are redacted before they leave the server, a write to a stage you don't own is refused, and secrets are never in the file (secretRef names them; the engine holds them). What the plugin keeps out of the browser is the workflow's code: how your forms, prompts and integrations are built.
Set it up
ts
// next.config.ts — Turbopack (the default) and webpack
import { withClipless } from '@clipless/react/next'
export default withClipless({
// your config
})ts
// vite.config.ts
import clipless from '@clipless/react/plugin'
export default defineConfig({
plugins: [clipless.vite(), react()],
})js
// webpack.config.js
const clipless = require('@clipless/react/plugin').default
module.exports = {
plugins: [clipless.webpack()],
}ts
import clipless from '@clipless/react/plugin'
clipless.rollup() // Rollup
clipless.rolldown() // Rolldown
clipless.esbuild() // esbuild
clipless.rspack() // Rspack, Rsbuild: clipless.rsbuild()
clipless.bun() // Bun.buildThat's all. Every workflow file your app imports now arrives without its action settings.
What the browser keeps
Every <Action> stays in place, reduced to what the browser uses:
| Kept | Why |
|---|---|
id | useAction('risk') and the ledger name it |
on | a field action is triggered by the browser (on blur, on a valid value) |
type, blocking, title | what the UI shows while it runs |
Everything else is removed: prompt, system, schema, endpoint, to, subject, body, templateUrl, secretRef, visibleTo and any setting added later. Constants that only a removed setting used, like a prompt kept in a variable, are removed with it.
One role per page
Most pages belong to one role: the employee files the claim, the manager reviews it. Add ?role= to the import and that page gets only the role's own stages:
tsx
import ClaimForm from './expense-claim.tsx?role=employee'
import ClaimReview from './expense-claim.tsx?role=manager'Or load the role's part when you know who's looking. Each role becomes its own chunk:
tsx
const byRole = {
'role:employee': lazy(() => import('./expense-claim.tsx?role=employee')),
'role:manager': lazy(() => import('./expense-claim.tsx?role=manager')),
}
const Claim = byRole[role]This isn't a security boundary: a browser renders only the stages its token's role owns, and the engine enforces it. It keeps other roles' forms out of the download.
Two rules make this work:
actoris a string.<Stage actor="role:manager">, not a variable. The plugin splits the file before any code runs.- Field actions have an
id.<Action id="vat_check" on="blur" … />inside a<Field>. The browser triggers it by id, and without one, the id depends on which stages are still in the file.
TypeScript
TypeScript doesn't know what ?role= means. Declare each role once, in any .d.ts file in your project:
ts
// clipless-env.d.ts
declare module '*?role=employee' {
const Workflow: import('react').ComponentType<Partial<import('@clipless/react').WorkflowProps>>
export default Workflow
}
declare module '*?role=manager' {
const Workflow: import('react').ComponentType<Partial<import('@clipless/react').WorkflowProps>>
export default Workflow
}Next.js
?role= imports need Next.js 16.2 or later under Turbopack. The rest of the plugin works from 16.0.
A ?role= import must not be where the server hands over to the client. Next drops the query when a server component imports a 'use client' file. Import the role from a client file instead:
tsx
// app/claims/[id]/review/claim-review.tsx
'use client'
export { default } from '@/workflows/expense-claim.tsx?role=manager'A workflow file that uses hooks (useWorkflowNav for a Continue button, say) is a client file itself, so this wrapper is the usual shape. A workflow file without hooks needs no 'use client': the Clipless components are client components already, and a server page can import it with ?role= directly.
Without the plugin
clipless build does the same strip ahead of time and writes one bundle per role:
bash
bunx clipless build expense-claim.tsx --out dist/cliplessImport dist/clipless/expense_claim_v1.manager.js where the manager's page needs it. See CLI.