How to reference a JSON parameter inside another JSON parameter?

Describe the problem/error/question

Hello. I am attempting to create a configuration module for a AI agentic workflow. The configuration node loads data from a filesystem and presents it as a JSON. The JSON elements make reference to other data in the workflow. A user would submit a query from a web page in the production system.

Part of user query from web page:
[
{
“query”: “What is the definition of recursive?”,
…..
}
]

Part of Configuration:
[
{
“QueryValidationAgent”: “{\n “contents”: [{\n “parts”: [{\n “text”: “{{ $json.body?.query || $json.query }}”\n }]\n }],\n “generationConfig”: {\n “response_mime_type”: “application/json”,\n “response_schema”: {\n “type”: “OBJECT”,\n “properties”: {\n “cleaned_context”: { “type”: “STRING”, “description”: “Extract the core query first.” },\n “reasoning”: { “type”: “STRING”, “description”: “Analyze if the context is sufficient.” },\n “image_request_detected”: { “type”: “BOOLEAN” },\n “status”: { “type”: “STRING”, “enum”: [“VALID_PROBLEM”, “MISSING_CONTEXT”, “INVALID”] }\n },\n “required”: [“cleaned_context”, “reasoning”, “image_request_detected”, “status”]\n }\n },\n “systemInstruction”: {\n “parts”: [{ “text”: “…” }]\n }\n}\n”
}
]

What is the error message (if any)?

I try to use the QueryValidationAgent JSON as the JSON body in a node like Edit Fields or an HTTP Request node as:
{{ $(‘Load Configuration’).item.json.ProblemValidationAgent }}

This loads the JSON fine. As can be seen above, the JSON contains a reference to the user query:
“text”: “{{ $json.body?.query || $json.query }}”

This JSON works if it is put raw into the JSON body of the node, filling in the query before passing along the output. But as a reference to Load Configuration, the query is undefined and left blank. Essentially, the JSON needs to be parsed before being passed on to the next node or the HTTP Request.

Please share your workflow

Share the output returned by the last node

Input:
{
“contents”: [{
“parts”: [{
“text”: “{{ $json.body?.query || $json.query }}”
}]
}],

}
Output:{
“contents”: [{
“parts”: [{
“text”: “”
}]
}],

}
Expected:
{
“contents”: [{
“parts”: [{
“text”: “What is the definition of recursive?”
}]
}],

}

How do I reference another JSON from a JSON used as a parameter to the JSON Body of a node?

Information on your n8n setup

  • n8n version: current cloud version
  • Database (default: SQLite):
  • n8n EXECUTIONS_PROCESS setting (default: own, main):
  • Running n8n via (Docker, npm, n8n cloud, desktop app): n8n cloud
  • Operating system:

Hi @richarda Welcome to the n8n community! This happens because n8n doesn’t auto-evaluate expressions stored inside ‘strings’ , once your JSON has something like "{{ $json.body?.query }}" inside it, it’s just plain text and n8n won’t re-run that expression later. The community thread shows that expressions injected as strings won’t be evaluated automatically by other nodes.

To fix this, you need to evaluate or substitute the value before the final node runs:

  • Build the JSON where you use it, put the {{ }} expression directly in the node that has the actual data.

  • Replace placeholders in a Code/Set node before sending.

  • Or explicitly evaluate the stored expression in a Code node if you really need dynamic expression evaluation.

Reference: You Can Take A Look Here

Thank you. I tried evaluating the expression in a Code node, using the reference format:
{{ $evaluateExpression({{ ${$json.condition} \}}) }}

This did not perform the variable substitution, however. So I am still lost as how to explicitly evaluate the expression to do the variable substitution. Any other tips to try?

Hi @richarda i think n8n doesn’t automatically evaluate expressions once they’re stored inside a JSON string. Since your config is loaded as a string, the {{ $json.query }} inside it is treated as plain text, which is why it comes through empty.

The best way to fix this is to force n8n to evaluate that string at runtime using the $evaluateExpression() function.

In your HTTP Request node (or Edit Fields), set the JSON Body to:

{{ $evaluateExpression($('Load Configuration').item.json.QueryValidationAgent) }}

Why this works:
This method tells n8n to “scan” the string from your config and execute any expressions it finds inside (like your query reference) before passing the final JSON to the API. It’s the cleanest way to turn that plain text back into a dynamic value

Thanks, Anshul, but this does not work. The preview of the results (under the JSON Body) shows that the substitution has happened, but in the HTTP Request itself the field is left blank, which ends up causing an error from the service.

This my first attempt at answering a post but I sympathize with your struggles around parameters. I hope this isn’t too long winded but here goes:-

  1. What is fundamentally wrong in the current workflow

There are two independent problems, and fixing only one will never work.

Problem A — expressions inside strings are inert

This field in Mock Input is the root cause:

“text”: “{{ $json.body?.query || $json.query }}”

Once this JSON is stored as a string value (type: “string”), n8n will never evaluate that moustache expression again.

By the time it reaches the HTTP node, this is just:

“text”: “”
This is not a bug. It is how n8n works by design.

Problem B — illegal moustache usage downstream

You are also violating your own moustache rule here:

{{ $(‘Load Configuration’).item.json.ProblemValidationAgent }}

Violations:

$() shorthand
.item
Implicit item context

Even if the expression were evaluated correctly, this approach is unsafe because it relies on implicit context and item-level resolution, which makes the workflow fragile, non-deterministic, and prone to breaking when execution order, item counts, or node structure change.

  1. The only correct architectural model in n8n

You must separate three concerns:

Concern Allowed to reference $json? Mutable?
Configuration :cross_mark: No :cross_mark: No
Runtime input :white_check_mark: Yes :white_check_mark: Yes
Request body :white_check_mark: Yes (once) :cross_mark: After bind

Right now, you are mixing all three.

  1. The correct solution (step-by-step, minimal changes)
    Step 1 — Fix the configuration (static, inert)

Your configuration must not contain $json at all.

Change QueryValidationAgent to this literal template:

{
“contents”: [
{
“parts”: [
{
“text”: “{{QUERY}}”
}
]
}
],
“generationConfig”: {
“response_mime_type”: “application/json”,
“response_schema”: {
“type”: “OBJECT”,
“properties”: {
“cleaned_context”: { “type”: “STRING” },
“reasoning”: { “type”: “STRING” },
“image_request_detected”: { “type”: “BOOLEAN” },
“status”: {
“type”: “STRING”,
“enum”: [“VALID_PROBLEM”, “MISSING_CONTEXT”, “INVALID”]
}
},
“required”: [
“cleaned_context”,
“reasoning”,
“image_request_detected”,
“status”
]
}
},
“systemInstruction”: {
“parts”: [{ “text”: “…” }]
}
}

No moustaches except placeholders.
No $json.
No logic.

Step 2 — Bind runtime data in Edit Fields (the boundary)

Replace your current Edit Fields logic with a single binding operation.

Edit Fields → set requestBody (type: object)
{{
…$node[“Load Configuration”].json[“ProblemValidationAgent”],
contents: [
{
parts: [
{
text: $json.query
}
]
}
]
}}

This does three critical things:

  1. Explicit node reference
  2. Runtime data bound exactly once
    3.Produces a final, executable JSON object

Step 3 — HTTP Request uses the bound object (no logic)

In Problem Validation Agent node:
{{ $json.requestBody }}

Nothing else.
No expressions inside strings.
No late evaluation.
No surprises.

  1. Why this is the only correct solution: You gain:-

Deterministic execution
Replay safety
Versionable agent configs
Zero hidden evaluation

What you avoid:-
Recursive moustache evaluation (n8n does not support it)
Dynamic expression execution (dangerous, brittle)
Silent data loss
“Why is this empty?” debugging forever

One-sentence rule to remember:
Configuration defines shape. Runtime injects values. Binding happens once, at the edge.

I hope this hopes.

Thanks, Michael, for the suggestion. That explains why what I was trying does not work. I am having trouble getting your proposed solution to work as well. It looks like the $node notation you recommend in step 2 is deprecated (Understanding the Differences Between $node and $() Expressions in n8n), and that is giving me a syntax error that I have not been able to resolve. I’ll keep working on variations to see if I can figure out the correct syntax.

Try this:-

Sorry, I think I have not explained well. The Configuration is loaded from a service, as a JSON. The parts of the configuration need to have references to the query (and possibly data from other services) that gets loaded later. What I am really trying to do is replaces the references with the actual user query that comes separately, before sending the result off to another service. Thus, I cannot manually put the configuration information into an Edit Fields node.

I have tried a few variations with Merge and Code nodes, but there does not seem to be a way to load a configuration file and modify its parameters for later use.

When I try your example, it is still treating the reference {{ $json.body?.query || $json.query }} as a literal string.

Got it, short answer: you have to hydrate your config in a Code node. If you load configuration JSON from an external source in n8n, any expressions inside it are treated as plain strings and are never evaluated. Expressions only run in node parameters, not inside loaded data, so Set or Merge nodes can’t fix this. To use runtime values, you must explicitly transform the JSON at runtime; most reliably by hydrating it in a Code node ( as provided, with the Code node commented for clarity) before sending it downstream.

Longer answer: to make sure I’m aligned with your use case:

You are loading a configuration JSON from a service in n8n.

Your config contained placeholders intended to reference a user query (and possibly other data) that arrived later in the workflow.

No matter what you tried (Set nodes, Merge nodes, expressions), placeholders like
{{ $json.body?.query || $json.query }} were always sent as literal strings.

The configuration was external and dynamic, so manually rebuilding it in an Edit Fields node didn’t work.


The real problem

The real problem is expecting n8n to evaluate expressions embedded inside externally loaded JSON. n8n does not evaluate expressions embedded inside externally loaded JSON, and there is currently no mechanism for it to do so. Understanding this distinction is important because it stops you from fighting n8n’s execution model. Once you internalize where expressions do and do not run, you can design workflows that work with the platform instead of against it and save hours of unnecessary frustration chasing fixes that can never work.

Expressions are only evaluated at the field level inside nodes. Once a value is inside JSON loaded from a service, it’s treated as plain data and no longer participates in expression evaluation.

This is why the placeholder was never replaced, regardless of node order or merges.

Merge / Set nodes can’t solve this.

They can: combine items, assign values, evaluate expressions at the field level.

They cannot: walk nested JSON or rewrite string contents, because expressions are only resolved where the node explicitly evaluates them.

So this isn’t a data-routing problem, it’s a template hydration problem.


Solution

The issue isn’t with the configuration data, but with the assumption about how n8n evaluates it.

Instead of using n8n expressions in the config:

  • Use neutral placeholders (e.g. {{query}} or QUERY) that are not n8n expressions

  • Use a Code node (yep, this sucks) to recursively walk the config and replace placeholders with runtime values. This works because at that point the JSON is just data, and you’re performing an explicit runtime transformation instead of relying on expression evaluation.

  • In n8n v2, run the Code node once per item and return the hydrated config as a single object per item

  • Send the hydrated JSON to the downstream HTTP request


Takeaway

If you need to modify externally loaded JSON using runtime values, n8n expressions won’t help because they are only evaluated in node parameters, not inside loaded data.

In practice, this means you must explicitly hydrate or transform that JSON at runtime, most reliably in a Code node.

There are workarounds (parameterizing earlier, fragment assembly), but for non-trivial configs they trade clarity and determinism for fragility.

If someone knows a cleaner, first-class mechanism in n8n for runtime JSON hydration, I’d genuinely like to learn it because today, I haven’t found a better solution.