@@ -317,3 +317,83 @@ records what executed structurally cannot record what did not.
317317failed runs with enough identity to be counted even without a transaction hash. If retention is
318318plan-dependent, say which plan buys what — this is the one endpoint whose whole value is that it
319319remembers.
320+
321+ ## DX-9 · ` matchesRegex ` is documented, generated by the builder, and rejected by the validator
322+
323+ ** Severity:** medium — a documented Condition operator cannot be used at all; the error message
324+ points somewhere else.
325+ ** Date:** 2026-09-11 · execution ` 4y5c1zst9hoegqkybyhvj `
326+
327+ ` docs/workflows/creating.md ` lists ` matchesRegex ` among the Condition operators. Our address gate
328+ used it: ` {{@trigger-1:Webhook.safeAddress}} matchesRegex ^0x[0-9a-fA-F]{40}$ ` . The first canvas
329+ run failed at that node with * "Cannot index "0x[ ...] ". Reference step outputs with the
330+ {{@nodeId : Label .field}} template format…"* — advice about template syntax, for an expression
331+ whose template syntax was fine.
332+
333+ Reading ` lib/workflow/nodes/condition/validator.ts ` explains why no spelling works:
334+
335+ - ` checkBracketExpressions ` matches ` (\w+)\s*\[ ` anywhere in the raw expression, ** inside string
336+ literals included** , so any character class preceded by a word character (` 0x[ ` , ` a-f[ ` ) is
337+ rejected as bare indexing.
338+ - The builder compiles the operator to ` new RegExp("…").test(String(…)) `
339+ (` condition/expression.ts:120 ` ), but ` DANGEROUS_PATTERNS ` contains ` /\bnew\s+\w/ ` and
340+ ` ALLOWED_METHODS ` has no ` test ` . The compiled form is banned twice.
341+
342+ So the operator exists in the docs and the UI, and every expression it produces fails validation.
343+ The tests in ` tests/unit/condition-builder-utils.test.ts ` cover generation and parsing, never
344+ evaluation.
345+
346+ ** Workaround:** ` startsWith("0x") && .length === 42 ` , with the strict regex in the Code node.
347+ ** Suggested fix:** mask string literals before the bracket and dangerous-pattern checks, and either
348+ allowlist ` new RegExp(...) ` /` .test ` for this operator or evaluate ` matchesRegex ` natively instead of
349+ compiling it to JS. Until then, remove it from the operator list — an operator that is documented
350+ and unusable costs more than one that is absent.
351+
352+ ## DX-10 · ` workflows/create ` stores ` workflowType: read ` regardless of nodes; only ` PATCH ` derives
353+
354+ ** Severity:** medium — combined with #2227 's freeze-at-listing, it is a trap with a misleading
355+ warning in front of it.
356+ ** Date:** 2026-09-11 · workflow ` 7v0qwhcp5gcex58gjugyg `
357+
358+ ` POST /api/workflows/create ` with a graph containing a ` web3/write-contract ` node returned a row
359+ with ` workflowType: "read" ` . ` validate_workflow ` then warned ` write-action-on-read-workflow ` —
360+ "confirm this is intentional" — on a workflow nobody had classified. A no-op
361+ ` PATCH /api/workflows/{id} ` with the same nodes flipped it to ` write ` , because the PATCH route
362+ derives from node content (#2227 ) and the create route does not.
363+
364+ Why it matters: #2227 freezes ` workflowType ` at the listing PATCH. A workflow created and listed
365+ without an intermediate save — the obvious agent flow, ` create_workflow ` → ` list_workflow ` — lists
366+ as ` read ` , and the Marketplace call then routes to ` handleReadWorkflow ` : the server executes with
367+ the ** owner's** wallet and returns results, instead of returning calldata for the caller to sign.
368+ For a workflow whose whole point is that the caller executes, that is the wrong contract, frozen.
369+
370+ ** Suggested fix:** derive on create exactly as on PATCH, or have ` validate_workflow ` say * "stored
371+ type was never derived — save once"* rather than asking the author to confirm a classification
372+ they did not make.
373+
374+ ## DX-11 · ` failOnError: false ` turns a reverted write into ` success: true ` at every level
375+
376+ ** Severity:** high for observability — the one outcome an executor most needs to see is the one
377+ that reports as success.
378+ ** Date:** 2026-09-11 · executions ` 819zav3gl3fkd89n7cb42 ` (won) and ` j9g0a58oxem50jct5qpt7 ` (lost)
379+
380+ Two concurrent triggers of the same workflow against the same Safe nonce. One broadcast and moved
381+ the money; the other's ` execTransaction ` reverted with ` Error(GS026) ` . The loser's ` exec-1 ` node
382+ log reads:
383+
384+ ```
385+ {"error": "Contract call failed: Error(GS026)", "success": true,
386+ "rejection": {"kind": "contract-custom", "name": "Error"}}
387+ ```
388+
389+ — ` success: true ` beside an ` error ` , node status ` success ` , execution status ` success ` ,
390+ ` transactionHashes: [] ` . The workflow's own reporting cannot distinguish "drained the Safe" from
391+ "reverted against it" without opening the node output and reading the error string. `failOnError:
392+ false` is the right setting here — a Safe revert is a named outcome, not a dead run — but the flag
393+ should suppress * abort* , not * truth* .
394+
395+ ** Suggested fix:** keep ` success ` meaning the call succeeded; surface the swallowed error as a
396+ distinct node status (` failed-continued ` , or ` status: success ` + ` error ` populated at the
397+ execution level), and count the run in ` analytics/runs ` with a ` revertReason ` even though it has
398+ no hash. DX-8 already notes that a failed run leaves no row; this is the same gap from the other
399+ side.
0 commit comments