LEARN / CORE CONCEPTS
Think in graphs.
Explicit steps. Explicit data flow. A validated acyclic graph makes your application logic inspectable before it runs.
Nodes & inputs
A node implements execute(input, context): Promise<NodeResult>. It returns an outputs dictionary, where each key is a named output port. Node types are trigger, action, logic, and custom: they describe roles, not external listeners. Calling engine.run() starts execution.
NodeInput has the run’s initial value and an ordered incoming array. Each delivery identifies its link, source node, source port, target port, and value. Inputs are not automatically merged. Validate the business shape your node expects.
NodeContext provides runId, nodeId, and an AbortSignal called signal.
Links & named ports
A link connects a source node’s output port to a target node’s input port. An optional synchronous or asynchronous transformation adapts data before delivery.
import type { ILink } from "@newvexa/workify";
const enrichedLink: ILink = {
id: "order-process",
name: "Enrich an order",
source: { nodeId: "order", port: "main" },
target: { nodeId: "process", port: "main" },
transform: (value, context) => {
context.signal.throwIfAborted();
return { order: value, priority: "high" };
},
};
This is a descriptor: add it only to a graph that contains order and process, with a target handler expecting the enriched value. LinkContext contains the run ID, link ID, source/target endpoints, and cancellation signal.
Conditional routing
ifNode emits the selected value on exactly one port: "true" or "false". Connect your action nodes to those ports.
import { ifNode } from "@newvexa/workify";
const route = ifNode({
id: "amount",
name: "Amount above 1,000",
value: (input) => input.incoming.length > 0
? input.incoming[0]!.value
: input.initial,
predicate: (value) => typeof value === "number" && value > 1000,
});
// Add route with workflow.addNode(route).
// Connect route's "true" and "false" ports to your action nodes.
switchNode selects an exact, case-sensitive case port or the reserved "default" port. Cases must be unique and nonempty; you cannot declare "default" as a case. The selector must return a string; an If predicate must return a boolean. Routing callbacks may be async.
The example checks entry presence rather than truthiness: valid delivered falsy values must not fall back to the initial input.
Roots, branches & joins
Every root—each node without incoming links—runs once per execution. A downstream node waits for all incoming links to become terminal. Delivered values arrive in link declaration order, regardless of completion timing.
- All incoming links inactive: the node is
skipped. - At least one delivered value and no failed dependency: the node executes.
- Any failed dependency: the node is
blocked, even if another branch delivered data.
Independent branches continue after another branch fails. A skipped path does not fail the run. Multiple links targeting the same port remain distinct incoming entries.
The JSON data contract
Payloads must be JsonValue: null, booleans, finite numbers, strings, dense arrays, and plain data objects. At each engine boundary, values are validated, recursively copied, and frozen.
Functions, cycles, class instances, undefined, accessors, symbols, sparse arrays, non-enumerable or extra array properties, and non-finite numbers are rejected. Convert dates to strings explicitly. JSON compatibility is not application-schema validation; check fields and domain constraints in your handlers.
Inspect your graph
workflow.findNodes({ type: "action", tags: ["orders", "billing"] });
workflow.findLinks({ sourceNodeId: "amount", name: "Order" });
workflow.search("order"); // { nodes, links }
Query fields combine with AND. Multiple tags or categories match with OR within their lists. IDs and node types match exactly; names and free text use case-insensitive substrings. Results retain declaration order.
workflow.toDefinition() keeps executable functions in memory. It is not JSON persistence or workflow recovery.