SuperAPI AI Directory

atscript-ui-wf

Frontend & websites

View repository

Updated 10-16 01:19

About this skill

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

```atscript
// 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.