Flow Function
Flow Function protocol schemas
@module automation/flow-function
The contract for a named handler function a script node invokes —
contributed by defineStack({ functions }) and resolved by name at execute
time (#1870).
The rule, and why it lives here instead of in a comment
A flow function is a PURE compute step: it receives its mapped input,
RETURNS a value, and the node's outputVariable exposes that value as a flow
variable so a later DECLARATIVE node persists it (update_record fields: { ai_category: '{aiResult.ai_category}' }). Data I/O stays on the flow graph.
That is not style advice. #4354's per-run summary reports what a run did to
the data, and the script node reports NO record metrics because of this
rule: every write a script causes is a downstream create_record /
update_record that counts itself, so "this node touched no records" is the
accurate answer rather than a guess. A function that writes anyway makes its
run under-report — selected: 30, acted: 0 on a run that wrote 30 invoices,
which reads exactly like the broken sweep #4354 exists to detect, and the
durable sys_automation_run row says so permanently.
Until #4396 that rule lived ONLY in a comment inside the executor, so neither an author, a lint, nor the runtime could see the contract the summary was relying on. It is now declared in two halves:
ActionDescriptor.handlerContract—scriptpublishes'pure', so the action catalog and the designer palette carry the rule an author reads.FlowFunctionEffectSchema— a function that legitimately writes DECLARES it, and its step then reportsunmeasuredEffect, so the run says "cannot count" instead of claiming it wrote nothing.
What is deliberately not here
A blanket unmeasuredEffect on every script step (the escape hatch #4354
gave connector_action) was rejected: it would suppress the broken-sweep
signal on every flow that calls any function, in order to accommodate the
flows that break the rule — paying for a rule-breaker with everyone else's
signal, and fossilizing the violation as supported behaviour.
Nor is this enforcement. The runtime hands a function no data reach —
FlowFunctionContext in @objectstack/service-automation carries
input / variables / automation / logger and no engine handle — but a
function is ordinary host code and can close over a client at module scope.
What the declaration buys is that the honest case is now expressible, and
the platform's own counters stop being wrong for it.
Source: packages/spec/src/automation/flow-function.zod.ts
TypeScript Usage
import { FlowFunctionEffectSchema } from '@objectstack/spec/automation';
import type { FlowFunctionEffect } from '@objectstack/spec/automation';
// Validate data
const result = FlowFunctionEffectSchema.parse(data);FlowFunctionEffect
What a script-node function does to data: 'pure' (computes and returns — the contract) or 'writes' (performs uncountable writes/effects, reported as unmeasured)
Allowed Values
purewrites