---
title: Migrate to Workflow-Owned UIs
description: Move a Gateway UI from launcher registration or .smithers/ui auto-mounting into a workflow-owned <UI> declaration.
---

Workflow UIs are now declared by the workflow itself with `<UI>`. The UI still
runs in the browser, still talks to the Gateway through `gateway-react`, and can
keep the same `.smithers/ui/<workflow>.tsx` file. The change is ownership: the
workflow TSX names the UI entry, so any Gateway that registers the workflow can
serve the right UI without out-of-band launcher code.

## Before

Older launchers mounted UI from the Gateway side:

```ts .smithers/gateway.ts
gateway.register("release", releaseWorkflow, {
  ui: {
    entry: ".smithers/ui/release.tsx",
    title: "Release",
  },
  entryFile: ".smithers/workflows/release.tsx",
});
```

Some workspaces relied on the old pack convention where a matching
`.smithers/ui/release.tsx` file was auto-mounted. That convention is replaced by
an explicit workflow declaration.

## After

Add `<UI>` inside the workflow's root `<Workflow>`:

```tsx .smithers/workflows/release.tsx
/** @jsxImportSource smithers-orchestrator */
import { createSmithers, UI } from "smithers-orchestrator";

const { Workflow, Task, smithers } = createSmithers({
  // schemas...
});

export default smithers((ctx) => (
  <Workflow name="release">
    <UI entry="../ui/release.tsx" title="Release" />
    <Task id="ship">{/* normal workflow work */}</Task>
  </Workflow>
));
```

Then register the workflow without a `ui` option:

```ts .smithers/gateway.ts
gateway.register("release", releaseWorkflow, {
  entryFile: ".smithers/workflows/release.tsx",
});
```

`entry` paths are resolved relative to the workflow file when `entryFile` is
provided. Existing self-mounting UI files that end with
`createGatewayReactRoot(<App />)` can stay as-is.

## Entry vs Source

Use `entry` for existing Gateway UI files:

```tsx
<UI entry="../ui/release.tsx" title="Release" />
```

Use `source` for a browser-safe module that exports a React component. The
Gateway mounts it with React and passes `props` plus boot metadata:

```tsx
<UI
  source="../ui/release-app.tsx"
  exportName="ReleaseApp"
  title="Release"
  props={{ mode: "operator" }}
/>
```

Do not point `source` at the workflow module itself. Workflow modules often
import Node/Bun APIs, agents, prompts, and database setup that cannot run in the
browser.

## Checklist

1. Add `UI` to the workflow's `smithers-orchestrator` import.
2. Add `<UI entry="../ui/<workflow>.tsx" title="..." />` as a direct child of
   the root `<Workflow>`.
3. Remove `ui: { entry, title }` from `gateway.register`; keep `entryFile`.
4. Leave the browser UI file in `.smithers/ui/<workflow>.tsx` unless you want to
   convert it to the `source` component-export shape.
5. Verify with `bunx smithers-orchestrator gateway` and open
   `/workflows/<workflow>` or run `bunx smithers-orchestrator ui RUN_ID`.

The Gateway discovers `<UI>` during `register()`. The component renders nothing
into the workflow graph and does not execute client code on the backend.
