CONTEXT.md
How the two client runtimes (legacy and modern) are mounted under the app's URL space, and the terms used to describe their path prefixes. Seeded while making the modern client's URL prefix runtime-configurable.
Legacy client:
The v1 client runtime (@nocobase/client, SchemaComponent), served at the app root.
Avoid: v1 (in user-facing terms), old client
Modern client:
The v2 client runtime (@nocobase/client-v2, FlowEngine / FlowModel), served under a dedicated URL prefix.
Avoid: v2 (in user-facing terms), new client
App public path:
The base URL path the whole NocoBase app is mounted under, set by APP_PUBLIC_PATH (default /).
Avoid: base path, root path
Site root:
The domain root path /, which may redirect into the App public path when the app is mounted under a sub-path.
Avoid: app root, public path
Modern client prefix:
The single URL path segment, directly under the app public path, where the modern client is served — set by APP_MODERN_CLIENT_PREFIX (default v, historically the hardcoded v2). A segment, not a full path; accepted in any of v / /v / /v/ and normalized to a bare segment.
Avoid: v2 prefix, route prefix, base url
Modern client public path:
The full URL prefix the modern client actually runs under = app public path + modern client prefix (e.g. /nocobase/ + v → /nocobase/v/). Used as the React Router basename (minus trailing slash) and injected to the browser as window.__nocobase_public_path__.
Avoid: v2 public path
Modern client build directory:
The fixed on-disk location of the modern client's built assets (dist/client/v/), named from DEFAULT_MODERN_CLIENT_PREFIX. Internal and never user-facing; intentionally does NOT track the runtime Modern client prefix, so the prefix can change at runtime without a rebuild.
Avoid: v2 dist, asset directory (without "build")
App client entry mode: The runtime policy that decides whether the legacy client shell keeps an entry on a given URL, or hands that entry off to the modern client; in production this is primarily enforced by the legacy browser shell, while dev may add targeted shortcuts. Avoid: route mode, bootstrap mode
Legacy-default: An App client entry mode where the app root opens the Legacy client, while the Modern client remains available at its own public path. Avoid: legacy mode, old default
Modern-default:
An App client entry mode where the app root itself hands off to the Modern client public path, while legacy deep links such as /admin or /signin remain valid.
Avoid: hybrid modern-only, redirect-all
Modern-only:
An App client entry mode where legacy client document entries hand off to the Modern client public path; in production this is mainly a browser-side handoff, while dev additionally avoids running the legacy client dev server and redirects non-/v/ entries into /v/.
Avoid: compatible modern-only, route-mapped modern-only
Client document entry request: An HTTP request whose job is to load a client HTML entry, not an API, websocket, upload, dist, or plugin-static resource. Avoid: all frontend request, browser request
//v//v/ entries are redirected into the modern dev entryDev: "If I deploy under
APP_PUBLIC_PATH=/nocobase/and setAPP_MODERN_CLIENT_PREFIX=admin, where does the modern client live?" Maintainer: "At/nocobase/admin/. The prefix is a single segment composed under the app public path — it does not replace it." Dev: "And the static build output folder?" Maintainer: "That is a separate concept from the URL prefix — the folder name stays fixed regardless of what the prefix is set to."
v); the modern client build directory is a fixed internal constant (v), decoupled from the prefix so the prefix can change at runtime without rebuilding (see ADR-0001).modern-only shortcut./ and the App public path. Resolved: entry-mode decisions apply to the App public path; the Site root may first redirect into it when the app is mounted under a sub-path.How workflow node plugins contribute their config UI and output variables, and how the same node definition serves both the legacy and modern canvases during the migration. Seeded while planning the modern-client canvas migration.
Instruction:
A workflow node type's client-side definition (e.g. query, condition, delay). A class that downstream plugins extend to register a node: it carries the node's static metadata (type, title, group, icon), its config UI, and its variable contributions. One Instruction instance per node type, held in the plugin's instruction registry.
Avoid: node class, node handler (that's the server concern)
Config UI: The form shown in a node's configuration drawer. Has two forms during migration: the legacy fieldset (a Formily schema, rendered by the legacy canvas) and the modern FieldsetLoader (a lazy loader of a plain React + antd component, rendered by the modern canvas). Avoid: node form, settings form
Legacy fieldset (fieldset, lowercase):
The Formily Record<string, ISchema> config form an Instruction has always carried. Pure data from the modern client's point of view — the modern canvas never interprets it; only the legacy canvas renders it through SchemaComponent.
Avoid: schema fieldset
Modern FieldsetLoader (FieldsetLoader):
A lazy loader — () => Promise<{ default: ComponentType }> — an Instruction optionally carries for the modern canvas (same LoaderOf shape as workflow trigger loaders). The loaded component is a plain React + antd form (no Formily) that reads/writes config.*. Its presence is the per-node migration switch: a node has migrated when it has a FieldsetLoader. (Distinguished from the legacy fieldset by field name, not letter case — see ADR-0003.)
Avoid: React fieldset, config component, Fieldset (the contract is now a loader)
Output variables (useVariables):
A hook each Instruction contributes describing the variables that node emits to downstream nodes (e.g. a query node emits the queried record's field tree). The core walks the current node's upstream chain, calls each upstream node's useVariables, and assembles the "Node result" branch of the variable tree. The contract keeps returning the legacy VariableOption shape during migration; the modern canvas adapts it to MetaTreeNode at the aggregation boundary.
Avoid: node variables (ambiguous with config-time vs run-time)
Workflow variable input:
The shared variable-picker embedded in node config forms, aggregating upstream-node outputs + trigger variables + scope variables + system variables + $env. The modern one reuses flow-engine's low-level VariableHybridInput (fed a workflow-constructed MetaTreeNode tree), not the top-level global VariableInput (whose tree is the global getPropertyMetaTree()). A downstream node author imports it from the workflow modern client and drops it in like any antd input — it reads the current node from NodeContext and the node list / workflow from FlowContext itself, so the author never wires context.
Avoid: variable selector, variable picker (use consistently if at all)
FlowContext (canvas-level):
The React context the modern canvas provides at its root, carrying { workflow, nodes, refresh } — the whole node list, the workflow record, and a refetch callback. Every canvas concern (branch traversal, add/drag/remove, variable aggregation) reads it. Mirrors the legacy canvas's FlowContext of the same shape.
Avoid: workflow context (collides with flow-engine's own FlowContext — this one is workflow-plugin-local)
NodeContext (node-level):
The React context the modern canvas wraps around a single node (card + config drawer), carrying the node object itself (with live upstream/downstream linked-list refs) — useNodeContext() returns that node. Owned/provided by the workflow core; a downstream node author neither imports nor provides it. The modern FieldsetLoader's loaded form renders inside it, and the shared workflow variable input consumes it (deriving upstreams via useAvailableUpstreams(node)). Mirrors the legacy NodeContext.Provider value={data} around the legacy Node.
Avoid: workflow context
src/client-v2/); the legacy canvas reaches it via the allowed v1 → v2 import direction. The legacy Formily rendering (SchemaComponent, Node, etc.) stays in src/client/.upstreams, etc.) from these two via hooks rather than receiving a merged context value.fieldset vs FieldsetLoader — distinguished by field name, not letter case (an earlier draft used case-sensitive fieldset/Fieldset; superseded by ADR-0003). fieldset = legacy Formily schema (data, pass-through); FieldsetLoader = modern lazy loader of a React form. The FieldsetLoader's presence is the per-node migration switch. (See ADR-0002 as amended by ADR-0003.)WorkflowNodeContext carrying { node, workflow, upstreams }. Resolved during canvas planning: align with v1's two-context split instead — FlowContext { workflow, nodes, refresh } at the canvas root + NodeContext = the node object at each node. workflow/upstreams are derived via hooks, not bundled into a node-context value.How workflow trigger plugins contribute trigger metadata, configuration forms, manual-execution inputs, and trigger variables during the migration.
Trigger:
A workflow trigger type's client-side definition (e.g. collection, schedule). A class registered by type that carries trigger metadata (title, description, sync mode), configuration UI, manual-execution UI, validation, trigger variables, and block-creation hooks.
Avoid: trigger option (too narrow), trigger handler (that's the server concern)
Trigger config UI: The form shown when configuring a workflow's trigger. Has two forms during migration: the legacy trigger fieldset (a Formily schema, rendered by legacy surfaces) and the modern trigger FieldsetLoader (a lazy loader of a plain React + antd component, rendered by modern surfaces). Avoid: workflow form, trigger settings (ambiguous with workflow metadata)
Legacy trigger fieldset (fieldset, presetFieldset, triggerFieldset):
The Formily schema maps a Trigger may carry for its three legacy surfaces: create-time preset config, trigger configuration, and manual execution variables. The modern client never interprets these schemas.
Avoid: trigger schema (too broad)
Modern trigger loaders (PresetFieldsetLoader, FieldsetLoader, TriggerFieldsetLoader):
Lazy loaders a Trigger may carry for the same three surfaces: create-time preset config, trigger configuration, and manual execution variables. The loaded components are plain React + antd forms and use the same loader naming convention as Instruction.
Avoid: createConfigFormLoader (retired name)
Trigger variables:
Variables contributed by the workflow's trigger under $context (for example schedule trigger time or trigger data). During migration the hook remains named useVariables and returns VariableOption; the modern variable aggregator adapts it to MetaTreeNode.
Avoid: context variables (too broad)
v1 -> v2 direction.createConfigFormLoader vs PresetFieldsetLoader — createConfigFormLoader was an early v2 registry option name. The trigger API now aligns with Instruction naming: PresetFieldsetLoader is the create-time trigger preset loader.The node-graph editor where a workflow's nodes are laid out, connected, added, removed, dragged, and configured. There are two parallel implementations during migration; this section names them and their shared substrate. Seeded while planning the canvas migration to client-v2.
Legacy canvas:
The v1 node-graph editor (src/client/: WorkflowCanvas, CanvasContent, Branch, Node), reached at /admin/settings/workflow/workflows/:id from the legacy settings list. Hand-rolled DOM + flexbox recursive render (no graph library); its config drawer / add-node menu / remove-branch modal are Formily.
Avoid: v1 canvas (in prose), old editor
Modern canvas:
The client-v2 node-graph editor (src/client-v2/), reached at /admin/workflow/workflows/:id from the WorkflowPane list. Renders the same node tree without Formily.
Avoid: v2 canvas (in prose), new editor
Parallel-worlds coexistence:
The two canvases are independent destinations over the same workflows + flow_nodes data, distinguished only by URL/entry list — not by any per-workflow flag. A workflow opens in whichever canvas its URL belongs to. The legacy canvas retires by deleting the legacy settings list + route once the modern canvas reaches parity.
Avoid: canvas toggle, canvas feature flag (there is none)
Runtime separation:
The legacy client runs at / and loads only each plugin's client entry; the modern client runs at /v/ and loads only each plugin's client-v2 entry. They never coexist in one browser runtime, so each has its own app/PluginManager and its own 'workflow' plugin instance. Relocating code to client-v2 is build-time source sharing (bundled into v1's own output); it is orthogonal to this runtime separation.
Avoid: shared runtime, single app instance (there are two, one per client)
Instruction registry (per-runtime):
Each runtime's 'workflow' plugin holds its own instruction registry, self-populated by node plugins' entries for that runtime (registerInstruction from client fills v1's; from client-v2 fills v2's) — mirroring the existing v2 trigger registry. The modern canvas reads only its own v2 registry (plugin.getInstruction(type)); a type registered only in v1 is omitted from the v2 add-node menu and renders a placeholder card if already present in a workflow.
Avoid: shared registry, cross-runtime instruction read (there is none)
Node tree:
The in-memory doubly-linked structure the canvas renders, built from the flat flow_nodes list by linkNodes() (sets live upstream/downstream refs). A branch is a node with branchIndex != null under a branching node (its upstreamId). Pure data — no Formily — so it ports verbatim to the modern canvas.
Avoid: node graph (reserve for the rendered view), node list (that's the flat form)
Block-creation menu item (getCreateModelMenuItem):
The Instruction method that lets a node's output be added as a data block inside a config drawer ("create block → node data → query data"). The v2-native counterpart of v1's useInitializers: same intent, but it returns a FlowModel SubModelItem (fed to the v2 sub-model menu, e.g. NodeDetailsModel) instead of a Formily SchemaInitializerItemType. It already exists in v2 — node authors do not migrate it, they keep both during transition.
Avoid: initializer (that's the v1 term useInitializers)
fieldset/FieldsetLoader switch (see Workflow Node Extension) — so the page-level canvas choice and the per-node config-UI choice are independent axes.{ node, workflow }, and emits a FlowModel-layer SubModelItem. The two layers stay decoupled: canvas context reaches the block model only via its inputArgs.FieldsetLoader presence). They compose; they are not the same switch.Unmigrated-node placeholder:
In the modern canvas, a node whose Instruction still has only fieldset (no FieldsetLoader) renders its card normally (topology is intact) but its config drawer shows a placeholder ("config UI not yet migrated"), not a Formily form. This keeps the modern canvas shippable before any config UI migrates — the two axes stay orthogonal. Rendering Formily as a fallback is forbidden (would drag the Formily runtime into client-v2).
Avoid: fallback form, legacy drawer (the modern canvas never renders fieldset)