From 32b0a88fe81a3145bf7ea301f16907a761fd295c Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 8 Oct 2026 22:13:14 -0700 Subject: [PATCH 1/2] fix(workflows): stop flagging whole-field references in JSON fields and correct the quoting advice --- .../editing/json-literal-refs.test.ts | 81 ++++++++++++++++++- apps/sim/lib/workflows/editing/lint.ts | 23 +++++- 2 files changed, 99 insertions(+), 5 deletions(-) diff --git a/apps/sim/lib/workflows/editing/json-literal-refs.test.ts b/apps/sim/lib/workflows/editing/json-literal-refs.test.ts index 4627a111bc0..8a290658ca1 100644 --- a/apps/sim/lib/workflows/editing/json-literal-refs.test.ts +++ b/apps/sim/lib/workflows/editing/json-literal-refs.test.ts @@ -63,6 +63,20 @@ const MOCK_BLOCKS = vi.hoisted( ], outputs: {}, }, + response: { + type: 'response', + category: 'blocks', + subBlocks: [ + { id: 'dataMode', type: 'dropdown' }, + { + id: 'data', + type: 'code', + language: 'json', + condition: { field: 'dataMode', value: 'json' }, + }, + ], + outputs: {}, + }, function: { type: 'function', category: 'blocks', @@ -135,7 +149,72 @@ describe('collectUnquotedJsonStringReferences', () => { value: [''], }) expect(findings[0]?.reason).toMatch(/^unquoted-json-string: /) - expect(findings[0]?.reason).toContain('""') + }) + + /** + * Quoting is only safe for text without a double quote, backslash, or line + * break, since the text is inserted raw; free text such as a model reply has + * to be built into JSON by a Function block instead. + */ + it('advises quoting only for plain text and a Function block for free text', () => { + const [finding] = collectUnquotedJsonStringReferences( + graph({ + start: START, + insert: insertRow('{"order_id": }'), + }) + ) + expect(finding?.reason).toContain('""') + expect(finding?.reason).toMatch(/double quote, backslash, or line break/) + expect(finding?.reason).toMatch(/Function block/) + expect(finding?.reason).not.toMatch(/Quote each one/) + }) + + /** + * A field that is exactly one reference is the referenced value itself: the + * Response block returns text it cannot parse as-is and the API block sends a + * text body raw, while quoting it would turn JSON text into a JSON string. + */ + it('accepts a field made only of string references, with no JSON around them', () => { + const findings = collectUnquotedJsonStringReferences( + graph({ + writer: { type: 'agent', name: 'Writer' }, + reply: { + type: 'response', + name: 'Reply', + subBlocks: { dataMode: { value: 'json' }, data: { value: '' } }, + }, + call: { + type: 'api', + name: 'Post', + subBlocks: { body: { value: ' \n' } }, + }, + signed: { + type: 'api', + name: 'Post Signed', + subBlocks: { body: { value: '\n\n' } }, + }, + }) + ) + expect(findings).toHaveLength(0) + }) + + it('still flags a string reference that sits inside JSON text around it', () => { + const findings = collectUnquotedJsonStringReferences( + graph({ + writer: { type: 'agent', name: 'Writer' }, + reply: { + type: 'response', + name: 'Reply', + subBlocks: { dataMode: { value: 'json' }, data: { value: '[]' } }, + }, + call: { + type: 'api', + name: 'Post', + subBlocks: { body: { value: '{"note": }' } }, + }, + }) + ) + expect(findings.map((finding) => finding.blockId)).toEqual(['reply', 'call']) }) it('flags every unquoted string reference in the field once, including agent text', () => { diff --git a/apps/sim/lib/workflows/editing/lint.ts b/apps/sim/lib/workflows/editing/lint.ts index b714bc002f2..2b2333dad00 100644 --- a/apps/sim/lib/workflows/editing/lint.ts +++ b/apps/sim/lib/workflows/editing/lint.ts @@ -758,13 +758,26 @@ export function collectDanglingBlockOutputReferences( * `block.path` bodies of the reference tokens in JSON text that sit outside * every string literal, tokenized with the runtime's own reference scanner. * Tokens inside a literal, escaped quotes included, are already strings in the - * parsed document. + * parsed document. A field made only of references has no JSON around them: + * the block receives the referenced text itself (Response returns text it + * cannot parse as-is, API sends it as a raw body), and quoting would only turn + * JSON text into a JSON string, so none of its tokens are reported. */ function unquotedJsonReferenceTokens(json: string): string[] { + const tokens = findWorkflowReferenceTokens(json) + let outsideTokens = '' + let previousEnd = 0 + for (const token of tokens) { + outsideTokens += json.slice(previousEnd, token.start) + previousEnd = token.end + } + outsideTokens += json.slice(previousEnd) + if (outsideTokens.trim() === '') return [] + const unquoted: string[] = [] let cursor = 0 let inString = false - for (const token of findWorkflowReferenceTokens(json)) { + for (const token of tokens) { for (; cursor < token.start; cursor++) { const char = json[cursor] if (inString && char === '\\') cursor++ @@ -784,7 +797,9 @@ function unquotedJsonReferenceTokens(json: string): string[] { * Outside Function code a reference is replaced by its raw text, so * `{"id": }` becomes `{"id": ord-1}` and the block fails to * parse it at run time, while lint, deploy, and every earlier run of a draft - * that never reached the block stay clean. Only references whose declared + * that never reached the block stay clean. No JSON-aware escaping exists + * outside Function code, so quoting is only safe for text that can never hold a + * quote, backslash, or line break. Only references whose declared * output type is `string` are reported: numbers, booleans, and objects already * resolve to JSON values, and an undeclared type cannot be judged here. */ @@ -835,7 +850,7 @@ export function collectUnquotedJsonStringReferences( field, value, kind: 'block-output', - reason: `unquoted-json-string: these references resolve to text, which is inserted without quotes, so the field is not valid JSON at run time unless the text is itself JSON. Quote each one, e.g. "${value[0]}".`, + reason: `unquoted-json-string: these references resolve to text, which is inserted raw, so the field is not valid JSON at run time unless the text is itself JSON. Quoting, e.g. "${value[0]}", works only for text that never contains a double quote, backslash, or line break, such as an id. For free text such as a model reply, build the JSON in a Function block, which reads references as values, and set this field to only that block's result reference.`, }) } } From 7864d23c7d391e04c9c7f506bcbde4cacba779c9 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 8 Oct 2026 22:26:13 -0700 Subject: [PATCH 2/2] fix(workflows): name JSON control characters in the quoting advice --- apps/sim/lib/workflows/editing/json-literal-refs.test.ts | 8 +++++--- apps/sim/lib/workflows/editing/lint.ts | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/sim/lib/workflows/editing/json-literal-refs.test.ts b/apps/sim/lib/workflows/editing/json-literal-refs.test.ts index 8a290658ca1..33717e16ea4 100644 --- a/apps/sim/lib/workflows/editing/json-literal-refs.test.ts +++ b/apps/sim/lib/workflows/editing/json-literal-refs.test.ts @@ -152,8 +152,8 @@ describe('collectUnquotedJsonStringReferences', () => { }) /** - * Quoting is only safe for text without a double quote, backslash, or line - * break, since the text is inserted raw; free text such as a model reply has + * Quoting is only safe for text without a double quote, backslash, or control + * character, since the text is inserted raw; free text such as a model reply has * to be built into JSON by a Function block instead. */ it('advises quoting only for plain text and a Function block for free text', () => { @@ -164,7 +164,9 @@ describe('collectUnquotedJsonStringReferences', () => { }) ) expect(finding?.reason).toContain('""') - expect(finding?.reason).toMatch(/double quote, backslash, or line break/) + expect(finding?.reason).toMatch( + /double quote, backslash, line break, or other control character/ + ) expect(finding?.reason).toMatch(/Function block/) expect(finding?.reason).not.toMatch(/Quote each one/) }) diff --git a/apps/sim/lib/workflows/editing/lint.ts b/apps/sim/lib/workflows/editing/lint.ts index 2b2333dad00..032c5ebd979 100644 --- a/apps/sim/lib/workflows/editing/lint.ts +++ b/apps/sim/lib/workflows/editing/lint.ts @@ -799,7 +799,7 @@ function unquotedJsonReferenceTokens(json: string): string[] { * parse it at run time, while lint, deploy, and every earlier run of a draft * that never reached the block stay clean. No JSON-aware escaping exists * outside Function code, so quoting is only safe for text that can never hold a - * quote, backslash, or line break. Only references whose declared + * quote, backslash, or control character. Only references whose declared * output type is `string` are reported: numbers, booleans, and objects already * resolve to JSON values, and an undeclared type cannot be judged here. */ @@ -850,7 +850,7 @@ export function collectUnquotedJsonStringReferences( field, value, kind: 'block-output', - reason: `unquoted-json-string: these references resolve to text, which is inserted raw, so the field is not valid JSON at run time unless the text is itself JSON. Quoting, e.g. "${value[0]}", works only for text that never contains a double quote, backslash, or line break, such as an id. For free text such as a model reply, build the JSON in a Function block, which reads references as values, and set this field to only that block's result reference.`, + reason: `unquoted-json-string: these references resolve to text, which is inserted raw, so the field is not valid JSON at run time unless the text is itself JSON. Quoting, e.g. "${value[0]}", works only for text that never contains a double quote, backslash, line break, or other control character, such as an id. For free text such as a model reply, build the JSON in a Function block, which reads references as values, and set this field to only that block's result reference.`, }) } }