ObjectStackObjectStack

Flow Function — Automation Protocol reference

The contract for a named handler function a script node invokes — contributed by defineStack({ functions }) and resolved by name at execute time.

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:

  1. ActionDescriptor.handlerContract — script publishes 'pure', so the action catalog and the designer palette carry the rule an author reads.
  2. FlowFunctionEffectSchema — a function that legitimately writes DECLARES it, and its step then reports unmeasuredEffect, 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 drop every flow that calls any function out of the broken-sweep FIRST FILTER (selected > 0 AND acted = 0 AND unmeasured = 0), in order to accommodate the flows that break the rule — paying for a rule-breaker with everyone else's measurement, and fossilizing the violation as supported behaviour. That predicate is a first filter and not a verdict (#12685: a healthy idempotent sweep that re-selects the same records and gates each one on "already handled" matches it on every run too, and what discriminates is the per-node fold in FlowRunSummary.nodes[] / gates[]). Dropping out of it is still costly even so: a run the filter never selects is never folded over either, so the escape hatch is declared per function rather than granted to every script step.

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, FlowFunctionLoweredDeclarationSchema } from '@objectstack/spec/automation';
import type { FlowFunctionEffect, FlowFunctionLoweredDeclaration } 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

  • pure
  • writes

FlowFunctionLoweredDeclaration

A lowered functions declaration: what the function declared about itself, with its callable replaced by a handler ref

Properties

PropertyTypeRequiredDescription
handlerstring✅The lowered handler ref (built artifacts) — the callable rides in the sibling ESM module
effectEnum<'pure' | 'writes'>optional (default: "pure")What the function does to data — omit for the pure default

On this page