Skip to content

A flow cannot REFUSE with per-record text: the only channel that interpolates is a screen description, and a message-only screen still renders Submit and toasts "completed" #14945

Description

@os-sales

Filed by the repo:hotcrm execution seat (session session_019hUuCQStzXGMFSX4dzww5t, R32). ⛔ Observation-level, unassigned, no domain:* / type — routing and grading belong to central triage. Filed here rather than worked around locally, per the hotcrm lane charter.

Surfaced while implementing a maintainer ruling that required a conversion to be refused with copy naming why and which record — hotcrm#1288, landed as hotcrm#1555. The behaviour shipped is correct; the shape available to express it is not.

The gap

Measured against @objectstack/spec / @objectstack/console 17.2.0, there is no metadata-authorable way for a flow to refuse an operation with per-record text. Every candidate channel fails on one axis:

channel why it cannot carry a per-record refusal
Action.visible / Action.disabled each is ZodOptional<ZodUnion<[ZodBoolean, CEL-envelope]>>no reason field of any kind. A hidden or greyed button cannot explain itself.
Action.errorMessage a single static string, and shown on a failed run.
a node executor's {success:false, error} the message is platform-authored, not authorable in metadata.
script node needs a host-registered function; not reachable from declarative metadata.
flow.errorMessage (console's error.details.errorMessage) again one static string for the whole flow.
object validations[] fires too late — in this case at the terminal write, after the account, contact and opportunity had already been created.

⇒ the only per-record channel is a screen node's config.description, which is interpolated per run and rendered by FlowRunner as the dialog body.

Why that channel is still the wrong shape

A message-only screen ({title, description}, no fields) is not a notice — it is an input step wearing a notice's clothes:

  • it renders Cancel + Submit, so a refusal offers the user a Submit button;
  • submitting resumes the run to end, and FlowRunner then toasts its neutral Flow "…" completed;
  • ⇒ a user who is being told "this is refused" clicks Submit and is told the flow completed.

⭐ One thing that does work correctly and is worth recording so nobody "fixes" it: the invoking action's own successMessage does not fire behind the dialog — a paused run returns {success: true, silent: true} and silent suppresses the action toast. Measured on the shipped console bundle (RecordDetailView flow handler). So the defect is the terminal toast and the Submit affordance, not a double-toast.

The behaviour is safe in the landed case regardless — every write in that flow sits behind the branch the refusal never reaches, so nothing is created on either path. This is about what an author can express, not a data-integrity bug.

What would close it

Either would do; ⛔ the seat is not proposing a design, only naming the shape:

  1. a terminal-notice screen variant — Close only, no Submit, and no completion toast; or
  2. a first-class refusal/stop node carrying authored, interpolated text, so "this run is refused, here is why" is a thing a flow can say.

Reproduction

src/flows/lead-conversion.flow.ts on objectstack-ai/hotcrm@f4068c4d, node refuse_confirmed_duplicate, reached by edge e25 out of decision_duplicate.

⚠️ A second, sharper finding from the same work, in case it is the same root: a decision node with no declared config.conditions takes every out-edge whose condition holds, in parallel. hotcrm#1555 caught this the hard way — a Clean edge spelled != "suspected" and a new == "confirmed" edge were both live for a confirmed record, so the refusal would have rendered and the conversion would have run in the same execution. It was found by an ablation, not by review, and the fix was to narrow the Clean condition into a true partition. ⇒ ⚠️ if a decision node's out-edges are intended to be exclusive, nothing in the platform enforces or warns about it, and the failure is silent and behavioural. That may deserve its own card; the seat is naming it here rather than splitting it, since central triage is better placed to judge whether they share a cause.

Activity

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

Metadata

Metadata

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions