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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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