Skip to content

FlowVariableSchema has no defaultValue, so a flow variable cannot be declared bound — the obvious remedy for an unbound-variable condition does not exist #651

Description

@os-zhuang

Found while implementing #643, which prescribed a remedy that turned out not to be expressible.

The gap

#643 asked that an unbound flow variable be fixed by "declaring the variable with a default so it is always bound", rather than by a has() guard. Measured against @objectstack/spec@17.0.0-rc.1, neither half of that is available:

1. The schema has no such key. FlowVariableSchema is z.core.$strict over exactly four fields:

declare const FlowVariableSchema: z.ZodObject< {
    name: z.ZodString;
    type: z.ZodString;
    isInput: z.ZodDefault< z.ZodBoolean >;
    isOutput: z.ZodDefault< z.ZodBoolean >;
}, z.core.$strict >;

Being strict, adding defaultValue to a flow variable is a parse error, not an ignored key.

2. The engine has no default-application step. AutomationEngine.execute binds a declared variable only when the caller supplied it:

if (flow.variables) {
  for (const v of flow.variables) {
    if (v.isInput && context?.params?.[v.name] !== void 0) {
      variables.set(v.name, context.params[v.name]);
    }
  }
}

So declaring a variable in flow.variables binds nothing at runtime. A declared input is unbound on every run whose caller did not pass it, and a strict-CEL condition reading it then aborts with No such key: <name> (Unknown variable: <name> for the bare spelling), the run is recorded failed, and the automation does not happen. That is the exact live defect #643 fixed in lead_conversion.

What #643 did instead, and why it is a workaround

lead_conversion now seeds the variable with an assignment node placed ahead of the screen that collects it:

{
  id: 'init_defaults', type: 'assignment', label: 'Default Conversion Options',
  config: { assignments: { createOpportunity: false } },
}

That achieves the right semantics — bound on every path, guaranteed by the graph rather than by the client — using the mechanism the platform actually has. But it is a workaround in three ways:

  • The default now lives twice: once on the screen field's defaultValue, once on the assignment node. Nothing keeps them in step.
  • It costs a node and an edge per defaulted variable, in a graph where nodes are the unit of review.
  • It would clobber a genuinely-supplied context.params value, because an assignment node is unconditional. Harmless here — the console's flow-action trigger sends only { recordId, objectName }, so createOpportunity cannot arrive as a param — but it is not the general shape.

test/flow-variable-conditions.test.ts pins the structural property (every variable a condition reads is bound on every path to it) so the gap cannot silently reappear, and its house-rule block records "declaring ≠ binding" as the trap it is.

What to establish

  1. Is defaultValue on a flow variable an intended-but-missing spec feature, or is the graph-binds-it model deliberate? If deliberate, the authoring story needs to say so — right now flow.variables reads exactly like a declaration that binds, which is what made this a defect rather than a known limitation.
  2. If it should exist: it belongs upstream in packages/spec (FlowVariableSchema) and in service-automation's execute/executeWithoutRetry seeding, applied before the start condition is evaluated so a start condition can read a defaulted variable too. Both halves are needed — a schema key with no engine support would be the inert-metadata failure of A decision node's singular config.condition is never evaluated — 24 flows author inert metadata #650.
  3. Decide whether a screen field's defaultValue should seed the flow variable of the same name at suspend time, which would remove the duplication above without any new spec key.

This is a platform-side gap; HotCRM can only work around it. Filing here so the workaround in lead_conversion has something to point at and can be removed when the platform closes it.

Filed unassigned.

Metadata

Metadata

Assignees

No one assigned

    Labels

    metadataDeclarative metadata — schema, security posture, UI surfacesprio:p2Triaged backlog — no near-term scheduleupstream:objectstackBlocked on / caused by the ObjectStack platform — tracked upstream

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions