atscript-ui-wf
Install
npx skills add moostjs/atscript-ui # installs all atscript-ui skills (this one + general + forms + tables + styles)
npx skills add moostjs/atscript # sibling — .as language
npx skills add moostjs/moostjs # sibling — Moost framework (decorators, interceptors)
npx skills add wooksjs/wooksjs # sibling — underlying @moostjs/event-wf engine + outlets
npx skills add moostjs/atscript-db # sibling — needed only if persisting state via AsWfStore
# Client
pnpm add @atscript/core @atscript/typescript @atscript/ui @atscript/vue-form @atscript/vue-wf vue
# Server
pnpm add @atscript/moost-wf moost @moostjs/event-http @moostjs/event-wf @prostojs/wf
pnpm add @atscript/db @atscript/db-sqlite # only if persisting via AsWfStore
Quick start
// src/wf/forms.as
export interface HelloName {
@meta.label 'Your name'
@meta.required 'Name is required'
name: string
}
// src/wf/hello.workflow.ts
import { Controller } from "moost";
import { Workflow, Step, WorkflowParam, WorkflowSchema } from "@moostjs/event-wf";
import { WfInput, finishWf } from "@atscript/moost-wf";
import { HelloName } from "./forms.as";
interface Ctx {
name?: string;
}
@Controller()
export class HelloWorkflow {
@Workflow("hello")
@WorkflowSchema<Ctx>([{ id: "ask" }, { id: "greet" }])
flow() {}
// @WfInput() validates against the schema and throws a StepRetriableError
// (caught natively by the wf engine) when input is missing or invalid —
// the handler body only runs with valid input.
@Step("ask")
async ask(@WfInput() input: HelloName, @WorkflowParam("context") ctx: Ctx) {
ctx.name = input.name;
}
@Step("greet")
greet(@WorkflowParam("context") ctx: Ctx) {
finishWf({ data: { greeting: `Hello, ${ctx.name}!` } });
}
}
<!-- src/views/HelloFlow.vue -->
<script setup lang="ts">
import { createDefaultTypes } from "@atscript/vue-form";
const types = createDefaultTypes();
function onFinished(r: { greeting: string }) {
alert(r.greeting);
}
</script>
<template>
<AsWfForm path="/wf/trigger" name="hello" :types="types" @finished="onFinished" />
</template>
Wire protocol
Client → server:
start { wfid, input?, wfs? }
submit { wfs, input }
action { wfs, action }
actionWithData { wfs, action, input }
Server → client:
next-step { inputRequired: { payload, transport: 'http', context }, wfs }
finished { finished: true, ...response }
outlet pause { sent: true } | { outlet: '<name>' }
error { error: { message, status? } }
Token transports: 'body' (default), 'cookie', 'query' (?wfs=...).
Invariants
| # |
Rule |
| 1 |
@WfInput() validates inbound payloads against the schema. Missing or invalid input throws StepRetriableError — the workflow engine catches it natively and re-pauses the step with the field errors. No global interceptor is required. Manual mid-handler revalidation: throw useAtscriptWf(Type).requireInput({ errors: { field: 'msg' } }). Standalone composable: useAtscriptWf(type) exposes resolveInput(), resolveAction(), and requireInput(). |
| 2 |
Context is server-only by default. Workflow context is a typed mutable per-flow object (typed via @WorkflowSchema<Ctx>). To expose keys to the client form annotate them on the FORM type with @wf.context.pass 'key'. serializeFormSchema() strips the annotation from the wire payload; extractPassContext(type, ctx) whitelists matching keys into inputRequired.context. Missing whitelist → empty context on the client. |
| 3 |
@wf.action.withData validates with deepPartial = true. Plain @ui.form.action 'id' actions take no input — @WfInput() rejects them unless declared with pass: true (the step opts into handling the no-data action). With-data actions receive partial form data — present fields validated, missing fields OK. |
| 4 |
Same-form re-validation preserves user input. The client compares the serialized payload identity between successive responses; same type back → render errors without remounting the form. formKey (the remount counter) increments only when the payload changes. |
| 5 |
Outlet pause is "finished" from the HTTP perspective. Server returns { sent: true } or { outlet: '<name>' } → client fires @finished. Actual resume happens out-of-band (email link click → URL with ?wfs=<token> → mount <AsWfForm initialToken="...">). No client-side polling. |
| 6 |
AsWfStore is single-use on resume. getAndDelete(handle) is race-safe — the row is deleted atomically when the resume succeeds. Don't call get() then delete() separately; use getAndDelete. |
| 7 |
Shadow columns require string | number | boolean. @wf.store.fromContext 'path.in.context' copies the value at every set(). Optional fields → null on path miss; required fields without DB defaults → insert may fail. Type mismatches log once per field per store instance, write null, continue. Run store.heal() after schema or path changes. |
| 8 |
@atscript/moost-wf/store is ESM-only. Triggered by any import of @atscript/moost-wf/store (runtime class) or @atscript/moost-wf/store.as (atscript model). Fix: set "type": "module" in the consumer's package.json and bundle ESM. CJS consumers must drop AsWfStore and use the in-memory store from @moostjs/event-wf. |
| 9 |
Token transport survives reloads only if persistent. body transport (default) is lost on reload. cookie survives until expiry. query (?wfs=token) is URL-shareable and single-use. Pick the transport that matches your resume story. |
| 10 |
HTTP outlet wraps inputRequired — conditionally. Mount createAsHttpOutlet() from @atscript/moost-wf (not bare createHttpOutlet) in handleAsOutletRequest. It wraps generic form payloads in { inputRequired: { payload, transport: 'http', context } } so <AsWfForm> decodes them. Pass-through: payloads already carrying a root-level routing key — finished, sent, outlet, error — flow through at the response root (merged with context if any), so outletHttp({ outlet: 'awaiting-payment' }) and outletHttp({ error: { message } }) keep working without a separate outlet. Bare createHttpOutlet flattens everything and crashes <AsWfForm> on form payloads with "Unexpected response format". |
| 11 |
Finished-response wrap. Use handleAsOutletRequest from @atscript/moost-wf (not bare handleWfOutletRequest from @moostjs/event-wf) as your trigger. It wraps the useWfFinished({ value }) unwrap so the response carries the finished: true marker <AsWfForm> requires. Pass-through for non-object responses (redirects, primitives), arrays, and already-marked envelopes (inputRequired / finished / error / sent / outlet). Step handlers complete via finishWf({ data: { ok: true, ... } }) (see invariant 12) — the helper builds the envelope and the wrap supplies the marker. |
| 12 |
Use WfFinished envelope helpers — not raw useWfFinished. @atscript/moost-wf ships two helpers: finishWf(opts?) and abortWf(reason, opts?). The shared FinishWfOpts bag carries { data?, message?, next? }; abortWf adds aborted: true + reason. The next field is the WfNext discriminated union ({ trigger: 'immediate' | 'auto' | 'manual', ... }) — the same shape rendered by <AsWfFinish>. Reach for raw useWfFinished().set({ type: 'data', value: envelope, cookies }) only when you need to set response cookies alongside the envelope — cookies are an HTTP-level concern the helpers don't expose. See finish-screens. |
| 13 |
<AsWfForm> exposes host-fired actions. A host fires a workflow action via a component ref or the default-slot actions bag: action(name, data?) (auto-classifies @wf.action.withData vs plain @ui.form.action) and supportsAction(name). Gate the host affordance on supportsAction — keyed on the declared action id (@ui.form.action id ∪ @wf.action.withData value), never the field name. Canonical use: a dialog's own Cancel button firing a hidden-but-whitelisted cancel. Custom shells classify via the public getDeclaredFormActions(def) from @atscript/ui. See client.md. |
Key imports
// Client
import { AsWfForm, useWfForm } from "@atscript/vue-wf";
import type { UseWfFormOptions, UseWfFormReturn } from "@atscript/vue-wf";
// Server — decorators, composables, helpers (full public surface of @atscript/moost-wf)
import {
WfInput,
WfAction,
useAtscriptWf,
useWfActionSlot,
serializeFormSchema,
extractPassContext,
getFormActions,
createAsHttpOutlet,
handleAsOutletRequest,
// WfFinished envelope helpers
finishWf,
abortWf,
isWfFinished,
} from "@atscript/moost-wf";
import type {
FinishWfOpts,
WfFinished,
WfMessage,
WfNext,
WfButton,
WfActionRequest,
} from "@atscript/moost-wf";
// Server — atscript build-time plugin (in atscript.config.ts)
import wfPlugin from "@atscript/moost-wf/plugin"; // default export — registers @wf.context.pass, @wf.action.withData, @wf.store.fromContext
// Server — persistent state store
import { AsWfStore, AsWfStateRecord } from "@atscript/moost-wf/store";
// Moost framework — @Controller, Resolve, Intercept, useControllerContext live here
import { Controller, Resolve, Intercept } from "moost";
// Workflow engine — @Workflow, @Step, @WorkflowSchema, @WorkflowParam,
// useWfState, useWfOutlet, outletEmail, outletHttp, StepRetriableError, etc. all live here.
// (To complete a step prefer `finishWf` / `abortWf` from `@atscript/moost-wf` above;
// raw `useWfFinished` from `@moostjs/event-wf` is the cookies escape hatch — see server.md.)
import {
Workflow,
Step,
WorkflowParam,
WorkflowSchema,
StepTTL,
useWfState,
useWfOutlet,
outlet,
outletEmail,
outletHttp,
} from "@moostjs/event-wf";
import { StepRetriableError } from "@wooksjs/event-wf";
References — load only what's needed
| Domain |
File |
When |
| First contact |
getting-started.md |
Install matrix, two-step "hello" flow end-to-end, minimal client mount |
| Server authoring |
server.md |
@Workflow / @Step / @WorkflowSchema from @moostjs/event-wf (linear vs branched), @WorkflowParam, @WfInput() auto-validation, useAtscriptWf().requireInput() pause signal, finishWf / abortWf to complete a step, conditional steps, action handlers (@WfAction), error mapping |
| Context |
context.md |
The workflow context object, mutation across steps, @wf.context.pass whitelist, extractPassContext, consuming formContext on the client, dynamic step titles via @ui.form.fn.title (cross-link to atscript-ui-forms dynamic-fields) |
| State persistence |
state.md |
AsWfStore({ table, clock?, actor? }) wiring, AsWfStateRecord base schema + extension with @meta.id, @wf.store.fromContext shadow columns (uses, limits, race-safe getAndDelete), cleanup(retention?), heal(options?) backfill, CJS limitation |
| Outlets / resume |
outlets.md |
Outlet semantics ({ sent: true }, { outlet: '<name>' }), email magic-link pattern with ?wfs=token resume, webhook resume, token transports (body / cookie / query) — when to pick which, initialToken prop |
| Client |
client.md |
<AsWfForm> props/emits/slots (@finished, @error, @form, @submit, @loading; slots #wf.loading — default is an as-form-overlay icon on a min-h-[100px] wrapper, #wf.error, #wf.finished, #form.*), useWfForm(options) composable (start / submit / action / actionWithData / retry), custom fetch for auth headers, host-fired actions (action / supportsAction) |
| Finish screens |
finish-screens.md |
WfFinished envelope, finishWf(opts) / abortWf(reason, opts) helpers, AsWfFinish trigger rendering (immediate / auto / manual), wf.finish.* scoped-slot contract with trigger callbacks, navigate prop (matches @atscript/db-client's Client({ navigate })), @dismiss / @action events |
| Recipes |
recipes.md |
When assembling a complete flow and needing the load-bearing wiring: login + MFA conditional branch, signup with inline OTP + mid-handler uniqueness errors, invite via email magic link (durable state, initialToken resume), checkout with save-draft action-with-data |
Customization
Workflow forms ride on top of <AsForm>, so the bulk of UI customization happens through the forms skill. The wf-specific surfaces:
- Tier 1 —
<AsWfForm> and <AsWfFinish> are the integration surface. Use useWfForm(options) for a fully custom shell.
- Tier 2 — the finish slots (
wf.finish.*) and the embedded <AsForm> defaults. The form's :types and :components propagate through <AsWfForm> straight to the form it renders.
- Server-side outlets — swap the transport (email magic link, webhook, awaiting payment, …) by mounting different outlet helpers; see outlets.md.
Propagate form customization through <AsWfForm>
The :types and :components props on <AsWfForm> are passed straight through to the underlying <AsForm>. Customize the same way you would a plain form:
<script setup lang="ts">
import { createDefaultTypes } from "@atscript/vue-form";
import MyTextInput from "./MyTextInput.vue";
import CountryPicker from "./CountryPicker.vue";
const types = { ...createDefaultTypes(), text: MyTextInput };
const components = { "country-picker": CountryPicker };
</script>
<template>
<AsWfForm path="/wf/trigger" name="signup" :types="types" :components="components" />
</template>
See atscript-ui-forms for the full :types / :components / AsFieldShell swap mechanics.
Customize finish + abort screens
<AsWfForm> renders <AsWfFinish> once the server emits a WfFinished envelope. Override message, primary CTA, countdown, dismiss button, or any per-option button via the wf.finish.* scoped slots. The server side ships envelopes via finishWf({ ... }) or abortWf(reason, { ... }); see finish-screens.md for the slot contract and WfNext discriminated union.
Swap outlets (server-side)
Outlets are mounted on the server controller. Replace the default createAsHttpOutlet() or add a custom outlet (outletEmail, outletHttp, your own) to change how the workflow pauses; clients reach the resume URL with <AsWfForm :initial-token="...">. See outlets.md.
Auth + custom fetch
Pass a fetch prop on <AsWfForm> to inject auth headers, cookies, or routing — the same way the table side accepts a clientFactory. The composable form is useWfForm({ fetch, ... }).
Style consequence
The form chrome inside <AsWfForm> is plain <AsForm>, so styles tree-shake the same way: replace a default field component and its as-* shortcuts drop. The finish screen's as-wf-finish-* shortcuts stay reachable as long as you use the default <AsWfFinish> rendering. See atscript-ui-styles for the per-domain shortcut groups (wfShortcuts).
See also
Reference docs: https://ui.atscript.dev/workflows/. Source: https://github.com/moostjs/atscript-ui.
1---2name: atscript-ui-wf3description: Build HTTP round-trip multi-step workflow forms with `@atscript/vue-wf` (client) + `@atscript/moost-wf` (server). Use when working with `<AsWfForm>` or `useWfForm` on the client; when declaring server flows via `@Workflow` / `@Step` / `@WorkflowSchema` / `@WfInput` / `@WfAction` on top of `@moostjs/event-wf`; when calling `useAtscriptWf(Type)` inside step handlers; when serializing forms (`serializeFormSchema`, `extractPassContext`) or passing context across steps via `@wf.context.pass`; when implementing action-with-data via `@wf.action.withData`; when persisting workflow state via `AsWfStore` + `@wf.store.fromContext` shadow columns; when implementing outlets (email magic link, webhook) with resume via `initialToken`; when emitting a `WfFinished` envelope via `finishWf` / `abortWf`; or when overriding the `<AsWfFinish>` finish-screen slots and `navigate` prop. Out of scope: plain forms (use `atscript-ui-forms`), tables (use `atscript-ui-tables`), styling (use `atscript-ui-styles`).4---56# atscript-ui-wf78## Install910```bash11npx skills add moostjs/atscript-ui # installs all atscript-ui skills (this one + general + forms + tables + styles)12npx skills add moostjs/atscript # sibling — .as language13npx skills add moostjs/moostjs # sibling — Moost framework (decorators, interceptors)14npx skills add wooksjs/wooksjs # sibling — underlying @moostjs/event-wf engine + outlets15npx skills add moostjs/atscript-db # sibling — needed only if persisting state via AsWfStore16```1718```bash19# Client20pnpm add @atscript/core @atscript/typescript @atscript/ui @atscript/vue-form @atscript/vue-wf vue2122# Server23pnpm add @atscript/moost-wf moost @moostjs/event-http @moostjs/event-wf @prostojs/wf24pnpm add @atscript/db @atscript/db-sqlite # only if persisting via AsWfStore25```2627## Quick start2829```atscript30// src/wf/forms.as31export interface HelloName {32 @meta.label 'Your name'33 @meta.required 'Name is required'34 name: string35}36```3738```ts39// src/wf/hello.workflow.ts40import { Controller } from "moost";41import { Workflow, Step, WorkflowParam, WorkflowSchema } from "@moostjs/event-wf";42import { WfInput, finishWf } from "@atscript/moost-wf";43import { HelloName } from "./forms.as";4445interface Ctx {46 name?: string;47}4849@Controller()50export class HelloWorkflow {51 @Workflow("hello")52 @WorkflowSchema<Ctx>([{ id: "ask" }, { id: "greet" }])53 flow() {}5455 // @WfInput() validates against the schema and throws a StepRetriableError56 // (caught natively by the wf engine) when input is missing or invalid —57 // the handler body only runs with valid input.58 @Step("ask")59 async ask(@WfInput() input: HelloName, @WorkflowParam("context") ctx: Ctx) {60 ctx.name = input.name;61 }6263 @Step("greet")64 greet(@WorkflowParam("context") ctx: Ctx) {65 finishWf({ data: { greeting: `Hello, ${ctx.name}!` } });66 }67}68```6970```vue71<!-- src/views/HelloFlow.vue -->72<script setup lang="ts">73import { createDefaultTypes } from "@atscript/vue-form";74const types = createDefaultTypes();75function onFinished(r: { greeting: string }) {76 alert(r.greeting);77}78</script>7980<template>81 <AsWfForm path="/wf/trigger" name="hello" :types="types" @finished="onFinished" />82</template>83```8485## Wire protocol8687```88Client → server:89 start { wfid, input?, wfs? }90 submit { wfs, input }91 action { wfs, action }92 actionWithData { wfs, action, input }9394Server → client:95 next-step { inputRequired: { payload, transport: 'http', context }, wfs }96 finished { finished: true, ...response }97 outlet pause { sent: true } | { outlet: '<name>' }98 error { error: { message, status? } }99100Token transports: 'body' (default), 'cookie', 'query' (?wfs=...).101```102103## Invariants104105| # | Rule |106| --- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |107| 1 | **`@WfInput()` validates inbound payloads against the schema.** Missing or invalid input throws `StepRetriableError` — the workflow engine catches it natively and re-pauses the step with the field errors. No global interceptor is required. Manual mid-handler revalidation: `throw useAtscriptWf(Type).requireInput({ errors: { field: 'msg' } })`. Standalone composable: `useAtscriptWf(type)` exposes `resolveInput()`, `resolveAction()`, and `requireInput()`. |108| 2 | **Context is server-only by default.** Workflow `context` is a typed mutable per-flow object (typed via `@WorkflowSchema<Ctx>`). To expose keys to the client form annotate them on the FORM type with `@wf.context.pass 'key'`. `serializeFormSchema()` strips the annotation from the wire payload; `extractPassContext(type, ctx)` whitelists matching keys into `inputRequired.context`. Missing whitelist → empty context on the client. |109| 3 | **`@wf.action.withData` validates with `deepPartial = true`.** Plain `@ui.form.action 'id'` actions take no input — `@WfInput()` rejects them unless declared with `pass: true` (the step opts into handling the no-data action). With-data actions receive partial form data — present fields validated, missing fields OK. |110| 4 | **Same-form re-validation preserves user input.** The client compares the serialized `payload` identity between successive responses; same type back → render errors without remounting the form. `formKey` (the remount counter) increments only when the payload changes. |111| 5 | **Outlet pause is "finished" from the HTTP perspective.** Server returns `{ sent: true }` or `{ outlet: '<name>' }` → client fires `@finished`. Actual resume happens out-of-band (email link click → URL with `?wfs=<token>` → mount `<AsWfForm initialToken="...">`). No client-side polling. |112| 6 | **`AsWfStore` is single-use on resume.** `getAndDelete(handle)` is race-safe — the row is deleted atomically when the resume succeeds. Don't call `get()` then `delete()` separately; use `getAndDelete`. |113| 7 | **Shadow columns require `string \| number \| boolean`.** `@wf.store.fromContext 'path.in.context'` copies the value at every `set()`. Optional fields → `null` on path miss; required fields without DB defaults → insert may fail. Type mismatches log once per field per store instance, write `null`, continue. Run `store.heal()` after schema or path changes. |114| 8 | **`@atscript/moost-wf/store` is ESM-only.** Triggered by any import of `@atscript/moost-wf/store` (runtime class) or `@atscript/moost-wf/store.as` (atscript model). Fix: set `"type": "module"` in the consumer's `package.json` and bundle ESM. CJS consumers must drop `AsWfStore` and use the in-memory store from `@moostjs/event-wf`. |115| 9 | **Token transport survives reloads only if persistent.** `body` transport (default) is lost on reload. `cookie` survives until expiry. `query` (`?wfs=token`) is URL-shareable and single-use. Pick the transport that matches your resume story. |116| 10 | **HTTP outlet wraps `inputRequired` — conditionally.** Mount `createAsHttpOutlet()` from `@atscript/moost-wf` (not bare `createHttpOutlet`) in `handleAsOutletRequest`. It wraps generic form payloads in `{ inputRequired: { payload, transport: 'http', context } }` so `<AsWfForm>` decodes them. **Pass-through:** payloads already carrying a root-level routing key — `finished`, `sent`, `outlet`, `error` — flow through at the response root (merged with `context` if any), so `outletHttp({ outlet: 'awaiting-payment' })` and `outletHttp({ error: { message } })` keep working without a separate outlet. Bare `createHttpOutlet` flattens everything and crashes `<AsWfForm>` on form payloads with "Unexpected response format". |117| 11 | **Finished-response wrap.** Use `handleAsOutletRequest` from `@atscript/moost-wf` (not bare `handleWfOutletRequest` from `@moostjs/event-wf`) as your trigger. It wraps the `useWfFinished({ value })` unwrap so the response carries the `finished: true` marker `<AsWfForm>` requires. Pass-through for non-object responses (redirects, primitives), arrays, and already-marked envelopes (`inputRequired` / `finished` / `error` / `sent` / `outlet`). Step handlers complete via `finishWf({ data: { ok: true, ... } })` (see invariant 12) — the helper builds the envelope and the wrap supplies the marker. |118| 12 | **Use `WfFinished` envelope helpers — not raw `useWfFinished`.** `@atscript/moost-wf` ships two helpers: `finishWf(opts?)` and `abortWf(reason, opts?)`. The shared `FinishWfOpts` bag carries `{ data?, message?, next? }`; `abortWf` adds `aborted: true` + `reason`. The `next` field is the `WfNext` discriminated union (`{ trigger: 'immediate' \| 'auto' \| 'manual', ... }`) — the same shape rendered by `<AsWfFinish>`. Reach for raw `useWfFinished().set({ type: 'data', value: envelope, cookies })` only when you need to set response cookies alongside the envelope — cookies are an HTTP-level concern the helpers don't expose. See [finish-screens](references/finish-screens.md). |119| 13 | **`<AsWfForm>` exposes host-fired actions.** A host fires a workflow action via a component `ref` or the default-slot `actions` bag: `action(name, data?)` (auto-classifies `@wf.action.withData` vs plain `@ui.form.action`) and `supportsAction(name)`. Gate the host affordance on `supportsAction` — keyed on the declared action **id** (`@ui.form.action` id ∪ `@wf.action.withData` value), never the field name. Canonical use: a dialog's own Cancel button firing a hidden-but-whitelisted `cancel`. Custom shells classify via the public `getDeclaredFormActions(def)` from `@atscript/ui`. See [client.md](references/client.md). |120121## Key imports122123```ts124// Client125import { AsWfForm, useWfForm } from "@atscript/vue-wf";126import type { UseWfFormOptions, UseWfFormReturn } from "@atscript/vue-wf";127128// Server — decorators, composables, helpers (full public surface of @atscript/moost-wf)129import {130 WfInput,131 WfAction,132 useAtscriptWf,133 useWfActionSlot,134 serializeFormSchema,135 extractPassContext,136 getFormActions,137 createAsHttpOutlet,138 handleAsOutletRequest,139 // WfFinished envelope helpers140 finishWf,141 abortWf,142 isWfFinished,143} from "@atscript/moost-wf";144import type {145 FinishWfOpts,146 WfFinished,147 WfMessage,148 WfNext,149 WfButton,150 WfActionRequest,151} from "@atscript/moost-wf";152153// Server — atscript build-time plugin (in atscript.config.ts)154import wfPlugin from "@atscript/moost-wf/plugin"; // default export — registers @wf.context.pass, @wf.action.withData, @wf.store.fromContext155156// Server — persistent state store157import { AsWfStore, AsWfStateRecord } from "@atscript/moost-wf/store";158159// Moost framework — @Controller, Resolve, Intercept, useControllerContext live here160import { Controller, Resolve, Intercept } from "moost";161162// Workflow engine — @Workflow, @Step, @WorkflowSchema, @WorkflowParam,163// useWfState, useWfOutlet, outletEmail, outletHttp, StepRetriableError, etc. all live here.164// (To complete a step prefer `finishWf` / `abortWf` from `@atscript/moost-wf` above;165// raw `useWfFinished` from `@moostjs/event-wf` is the cookies escape hatch — see server.md.)166import {167 Workflow,168 Step,169 WorkflowParam,170 WorkflowSchema,171 StepTTL,172 useWfState,173 useWfOutlet,174 outlet,175 outletEmail,176 outletHttp,177} from "@moostjs/event-wf";178import { StepRetriableError } from "@wooksjs/event-wf";179```180181## References — load only what's needed182183| Domain | File | When |184| ----------------- | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |185| First contact | [getting-started.md](references/getting-started.md) | Install matrix, two-step "hello" flow end-to-end, minimal client mount |186| Server authoring | [server.md](references/server.md) | `@Workflow` / `@Step` / `@WorkflowSchema` from `@moostjs/event-wf` (linear vs branched), `@WorkflowParam`, `@WfInput()` auto-validation, `useAtscriptWf().requireInput()` pause signal, `finishWf` / `abortWf` to complete a step, conditional steps, action handlers (`@WfAction`), error mapping |187| Context | [context.md](references/context.md) | The workflow context object, mutation across steps, `@wf.context.pass` whitelist, `extractPassContext`, consuming `formContext` on the client, dynamic step titles via `@ui.form.fn.title` (cross-link to atscript-ui-forms dynamic-fields) |188| State persistence | [state.md](references/state.md) | `AsWfStore({ table, clock?, actor? })` wiring, `AsWfStateRecord` base schema + extension with `@meta.id`, `@wf.store.fromContext` shadow columns (uses, limits, race-safe `getAndDelete`), `cleanup(retention?)`, `heal(options?)` backfill, CJS limitation |189| Outlets / resume | [outlets.md](references/outlets.md) | Outlet semantics (`{ sent: true }`, `{ outlet: '<name>' }`), email magic-link pattern with `?wfs=token` resume, webhook resume, token transports (`body` / `cookie` / `query`) — when to pick which, `initialToken` prop |190| Client | [client.md](references/client.md) | `<AsWfForm>` props/emits/slots (`@finished`, `@error`, `@form`, `@submit`, `@loading`; slots `#wf.loading` — default is an `as-form-overlay` icon on a `min-h-[100px]` wrapper, `#wf.error`, `#wf.finished`, `#form.*`), `useWfForm(options)` composable (`start` / `submit` / `action` / `actionWithData` / `retry`), custom `fetch` for auth headers, host-fired actions (`action` / `supportsAction`) |191| Finish screens | [finish-screens.md](references/finish-screens.md) | `WfFinished` envelope, `finishWf(opts)` / `abortWf(reason, opts)` helpers, `AsWfFinish` trigger rendering (`immediate` / `auto` / `manual`), `wf.finish.*` scoped-slot contract with `trigger` callbacks, `navigate` prop (matches `@atscript/db-client`'s `Client({ navigate })`), `@dismiss` / `@action` events |192| Recipes | [recipes.md](references/recipes.md) | When assembling a complete flow and needing the load-bearing wiring: login + MFA conditional branch, signup with inline OTP + mid-handler uniqueness errors, invite via email magic link (durable state, `initialToken` resume), checkout with save-draft action-with-data |193194## Customization195196Workflow forms ride on top of `<AsForm>`, so the bulk of UI customization happens through the forms skill. The wf-specific surfaces:197198- **Tier 1** — `<AsWfForm>` and `<AsWfFinish>` are the integration surface. Use `useWfForm(options)` for a fully custom shell.199- **Tier 2** — the finish slots (`wf.finish.*`) and the embedded `<AsForm>` defaults. The form's `:types` and `:components` propagate through `<AsWfForm>` straight to the form it renders.200- **Server-side outlets** — swap the transport (email magic link, webhook, awaiting payment, …) by mounting different outlet helpers; see [outlets.md](references/outlets.md).201202### Propagate form customization through `<AsWfForm>`203204The `:types` and `:components` props on `<AsWfForm>` are passed straight through to the underlying `<AsForm>`. Customize the same way you would a plain form:205206```vue207<script setup lang="ts">208import { createDefaultTypes } from "@atscript/vue-form";209import MyTextInput from "./MyTextInput.vue";210import CountryPicker from "./CountryPicker.vue";211212const types = { ...createDefaultTypes(), text: MyTextInput };213const components = { "country-picker": CountryPicker };214</script>215216<template>217 <AsWfForm path="/wf/trigger" name="signup" :types="types" :components="components" />218</template>219```220221See [atscript-ui-forms](../atscript-ui-forms/SKILL.md) for the full `:types` / `:components` / `AsFieldShell` swap mechanics.222223### Customize finish + abort screens224225`<AsWfForm>` renders `<AsWfFinish>` once the server emits a `WfFinished` envelope. Override message, primary CTA, countdown, dismiss button, or any per-option button via the `wf.finish.*` scoped slots. The server side ships envelopes via `finishWf({ ... })` or `abortWf(reason, { ... })`; see [finish-screens.md](references/finish-screens.md) for the slot contract and `WfNext` discriminated union.226227### Swap outlets (server-side)228229Outlets are mounted on the server controller. Replace the default `createAsHttpOutlet()` or add a custom outlet (`outletEmail`, `outletHttp`, your own) to change how the workflow pauses; clients reach the resume URL with `<AsWfForm :initial-token="...">`. See [outlets.md](references/outlets.md).230231### Auth + custom fetch232233Pass a `fetch` prop on `<AsWfForm>` to inject auth headers, cookies, or routing — the same way the table side accepts a `clientFactory`. The composable form is `useWfForm({ fetch, ... })`.234235### Style consequence236237The form chrome inside `<AsWfForm>` is plain `<AsForm>`, so styles tree-shake the same way: replace a default field component and its `as-*` shortcuts drop. The finish screen's `as-wf-finish-*` shortcuts stay reachable as long as you use the default `<AsWfFinish>` rendering. See `atscript-ui-styles` for the per-domain shortcut groups (`wfShortcuts`).238239## See also240241Reference docs: https://ui.atscript.dev/workflows/. Source: https://github.com/moostjs/atscript-ui.