Skip to content

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.build

That'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:

KeptWhy
iduseAction('risk') and the ledger name it
ona field action is triggered by the browser (on blur, on a valid value)
type, blocking, titlewhat 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:

  • actor is 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/clipless

Import dist/clipless/expense_claim_v1.manager.js where the manager's page needs it. See CLI.