windmill-labs/windmill · error

Invalid JSON after replacement: ${message}

Error message

Invalid JSON after replacement: ${message}

What it means

The patch_flow_json copilot tool applies a string find-and-replace to the serialized flow JSON, then re-parses it. If JSON.parse fails after the replacement, the tool throws this error wrapping the parser's message. It means the old_string/new_string pair produced syntactically invalid JSON — usually an unbalanced brace, quote, or comma introduced by the patch.

Source

Thrown at frontend/src/lib/components/copilot/chat/flow/core.ts:564

			toolCallbacks.setToolStatus(toolId, {
				content: 'Applying JSON patch...'
			})

			const updatedFlowJson = findAndReplace(
				currentFlowJson,
				oldString,
				newString,
				replaceAll,
				'current flow JSON'
			)

			let patchedValue: unknown
			try {
				patchedValue = JSON.parse(updatedFlowJson)
			} catch (error) {
				const message = error instanceof Error ? error.message : String(error)
				throw new Error(`Invalid JSON after replacement: ${message}`)
			}
			const aiProviders = await getAiAgentProviderCatalogFor(
				workspace,
				(patchedValue as { modules?: unknown } | null)?.modules
			)
			const aiProviderWarnings: string[] = []
			// Validation errors carry their own diagnosis (a bad module, a provider the workspace
			// does not have); only a parse failure is about the replacement's JSON.
			const parsedFlow: EditableFlowJson = validateEditableFlowJson(patchedValue, {
				aiProviders,
				aiProviderWarnings
			})

			for (const [moduleId, content] of Object.entries(inlineScriptSession.getAll())) {
				helpers.inlineScriptSession.set(moduleId, content)
			}

			const updateResult = await helpers.setFlowJson({

View on GitHub (pinned to e474e8803c)

Solutions

  1. Re-run patch_flow_json with old_string/new_string chosen so both sides are valid JSON fragments (keep quotes/braces balanced).
  2. Use smaller, structural replacements (a single field value) rather than large multi-object spans.
  3. Set replace_all when the fragment occurs more than once, or include surrounding context to make the match unique.
  4. Read the current flow JSON first (get flow state) and base the patch on its exact serialized text, including escaping.

Example fix

// before: breaks JSON — unquoted key in new_string
patchFlowJson({ old_string: '"timeout": 30', new_string: 'timeout: 60' })
// after: valid JSON fragment
patchFlowJson({ old_string: '"timeout": 30', new_string: '"timeout": 60' })
Defensive patterns

Strategy: validation

Validate before calling

// validate the patch result before handing it to the tool
const patched = findAndReplace(currentJson, oldString, newString, replaceAll, 'flow JSON')
try { JSON.parse(patched) } catch (e) { throw new Error(`Patch would produce invalid JSON: ${e.message}`) }

Type guard

function isValidJson(s: string): boolean {
  try { JSON.parse(s); return true } catch { return false }
}

Try / catch

try {
  await patchFlowJson({ old_string, new_string, replace_all })
} catch (e) {
  if (e.message.startsWith('Invalid JSON after replacement')) {
    // fall back to a targeted set_module_code / smaller patch
    await applySmallerPatch()
  } else throw e
}

Prevention

When it happens

Trigger: patch_flow_json where replacing old_string with new_string breaks JSON syntax: quoting a fragment that includes JSON quotes, deleting a comma or brace, replacing only one of two occurrences without replace_all, or new_string containing unescaped control characters.

Common situations: The model patches a snippet containing escaped quotes (\") but supplies raw quotes in new_string; a replacement spans an object boundary; replace_all omitted when the target appears multiple times, leaving the JSON inconsistent.

Understand the failure class

Related errors


AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03). Data as JSON: /api/errors/099fa55d404c5ae1. Report an issue: GitHub.