Skip to content

fix(flows): translate n8n $json bindings to the real engine scope root - #5285

Merged
graycyrus merged 1 commit into
tinyhumansai:mainfrom
graycyrus:fix/flows-n8n-json-item-binding
Jul 31, 2026
Merged

graycyrus merged 1 commit into
tinyhumansai:mainfrom
graycyrus:fix/flows-n8n-json-item-binding

Conversation

@graycyrus

@graycyrus graycyrus commented Jul 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Fixes a silent data-loss bug in the n8n importer: every imported workflow using $json expressions saved cleanly, reported no warnings, and then ran hollow.
  • ={{ $json.email }} now translates to =.item.email (was =.email, which resolves to null at runtime). ={{ $json }} becomes =.item (was =.).
  • Warns on duplicate n8n node names, which previously mis-wired edges silently.
  • Corrects an existing test that pinned the bug, plus a second test that was quietly relying on the broken output.

Problem

The tinyflows expression scope root is {item, items, run, nodes} (see expr_scope_for in vendor/tinyflows/src/nodes/mod.rs). n8n's $json means the current input item, which the engine binds as item.

The importer translated the $json tail directly onto the scope root, so $json.email became =.email — dereferencing a key that does not exist at the root. jq yields null rather than erroring, so:

  1. The import reported no warning (these took the "trivially translatable" clean path).
  2. The graph saved and enabled normally.
  3. Every translated binding resolved null at runtime, producing empty tool args and hollow runs.

This is exactly the silent-null-binding class the domain's propose/save gates (validate_binding_resolvability, the dry_run_workflow null-resolution gate) were built to prevent — and the importer bypassed all of them.

Worse, translates_trivial_json_expression_to_jq asserted =.email and warnings.is_empty(), so the bug was pinned as intended behaviour.

Solution

  • json_path_to_jq returns the tail path; translate_expr prepends .item at the single call site, so the empty-tail case naturally becomes .item with no special-casing.
  • Bracket-key quoting is preserved: ={{ $json["first name"] }} → =.item."first name".
  • Duplicate-name collisions in name_to_id now push a warning naming both ids, matching how every other approximation in this importer already behaves.

Deliberately forward-only. Already-imported flows keep their broken bindings. Auto-rewriting stored graphs was rejected: it cannot distinguish an importer artifact from a hand-authored root-level jq program, so it risks corrupting valid graphs. Detecting affected saved graphs via the existing validation banner is the suggested follow-up.

Worth reviewers' attention: validate_binding_resolvability's regex only matches =nodes.<ref>.item... cross-node references — it does not statically inspect plain .item.<field> bindings, so that gate would not have caught this on its own. The tests that actually prove the fix evaluate a translated expression against a realistic {item, items, run, nodes} scope, keeping the old form as a negative control that resolves null.

Submission Checklist

  • Tests added or updated (happy path + at least one failure / edge case) per Testing Strategy
  • Diff coverage ≥ 80% — small, directly-tested diff in a single module; cargo test --lib openhuman::flows::n8n_import = 18 passed, 0 failed
  • Coverage matrix updated — N/A: behaviour-only bug fix, no feature rows added/removed/renamed
  • All affected feature IDs from the matrix are listed in the PR description under ## Related — N/A: no matrix feature rows affected
  • No new external network dependencies introduced
  • Manual smoke checklist updated if this touches release-cut surfaces — N/A: importer-internal expression translation, not a release-cut surface
  • Linked issue closed via Closes #NNN — N/A: found by code review, no tracking issue filed yet

Impact

  • Runtime/platform: Rust core only (openhuman::flows::n8n_import). No frontend, no CLI surface change.
  • Compatibility: newly-imported workflows now bind correctly. Previously-imported workflows are unchanged and still carry the broken bindings — see the forward-only note above.
  • Security/migration: none. No schema change, no Cargo.toml/Cargo.lock change.

Related

  • Closes: N/A
  • Follow-up PR(s)/TODOs: surface already-imported flows with unresolvable bindings via the existing validation banner.
  • Merge order: independent of the other flows PRs in this batch — touches only n8n_import.rs, no file overlap.

AI Authored PR Metadata (required for Codex/Linear PRs)

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: fix/flows-n8n-json-item-binding
  • Commit SHA: ca8e4575f

Validation Run

  • pnpm --filter openhuman-app format:check — N/A, no frontend files changed
  • pnpm typecheck — N/A, no TypeScript changed
  • Focused tests: GGML_NATIVE=OFF cargo test --lib openhuman::flows::n8n_import → 18 passed, 0 failed
  • Rust fmt/check (if changed): GGML_NATIVE=OFF cargo check --manifest-path Cargo.toml clean
  • Tauri fmt/check (if changed): N/A, app/src-tauri untouched

Validation Blocked

  • command: N/A
  • error: N/A
  • impact: N/A

Behavior Changes

  • Intended behavior change: n8n $json expressions translate to the real engine scope root, so imported bindings resolve real values instead of null.
  • User-visible effect: newly-imported n8n workflows actually work. Duplicate node names now produce a visible import warning instead of silently mis-wiring edges.

Parity Contract

  • Legacy behavior preserved: all other translation paths (non-$json, untranslatable =-expressions, unmapped node types, extra-trigger demotion) are unchanged and still warn exactly as before.
  • Guard/fallback/dispatch parity checks: the untranslatable-expression fallback (raw passthrough + warning) is untouched; only the $json fast path changed.

Duplicate / Superseded PR Handling

  • Duplicate PR(s): none
  • Canonical PR: this one
  • Resolution: N/A

Summary by CodeRabbit

  • Bug Fixes
    • Improved $json expression handling for more reliable data access in imported workflows.
    • Added warnings when imported nodes have duplicate names, including the conflicting identifiers.
    • Preserved connection behavior when duplicate node names are encountered.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The n8n importer now warns about duplicate node names while retaining last-name-wins wiring. $json expressions now resolve against the tinyflows item scope. Tests cover path translation, graph validity, binding resolvability, and runtime evaluation.

Changes

n8n import behavior

Layer / File(s) Summary
Duplicate node-name handling
src/openhuman/flows/n8n_import.rs
Duplicate names now emit user-facing warnings. Both nodes remain in the graph, and connection lookup retains last-name-wins behavior. Tests verify warning output and graph validity.
Item-scoped expression translation
src/openhuman/flows/n8n_import.rs
$json paths now translate relative to .item, including bare $json. Expression, HTTP mapping, and end-to-end tests verify correct binding and runtime resolution.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: al629176

Poem

A rabbit hops through .item bright,
And duplicate names now warn just right.
Paths bind cleanly, graphs stay sound,
Tests chase values all around.
“Import success!” the bunny sings.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the primary change: translating n8n $json bindings to the engine's .item scope root.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@graycyrus
graycyrus force-pushed the fix/flows-n8n-json-item-binding branch from ca8e457 to 1a1187c Compare July 30, 2026 19:09
@graycyrus

Copy link
Copy Markdown
Contributor Author

Review follow-up: clean, no changes applied — the translation was independently verified correct for every shape (dotted paths, nested dot+bracket, quoted keys, bare {{ $json }}); anything that isn't a clean dotted/bracket path falls through to the safe raw+warn path rather than translating wrongly. The corrected tests were confirmed non-tautological: they evaluate through the real tinyflows::expr engine, and the negative control genuinely resolves null.

One real gap found, recommended as a follow-up rather than a blocker.

.item.<field> is correct immediately downstream of every node kind this importer emits except httpRequest. HttpRequestNode::execute always wraps its result in the {json, text, raw} envelope, so for a node directly downstream of an imported HTTP Request, the current item is the envelope — and a top-level .item.email dereferences a key that does not exist, resolving null. That is the same failure class this PR fixes, one hop further along, and it hits a very common n8n shape: Webhook → HTTP Request → Set/If/Code reading $json.<field>.

Two things make it a follow-up rather than a fix here:

  1. The existing ENVELOPING_KINDS gate in validate_binding_resolvability only fires for a tool_call's args addressed via the cross-node =nodes.<ref>.item... form. It does not scan a plain direct-predecessor .item.<field> binding, which is exactly what the importer emits — so neither this PR nor the existing gate catches it.
  2. Even .item.json.<field> would not be enough today: OpenHumanHttp::request returns {"text": ...}, so the response body is never decomposed into structured fields. A real fix needs either response-body JSON parsing at the capability layer or a fromjson step — plus topology awareness the importer lacks (node configs are translated before map_connections builds the edge graph).

This PR is strictly better than before in every case — the pre-fix code was wrong here too, missing both .item and .json — so it does not regress anything. Filing the envelope gap separately.

…oot (R-C1)

The n8n importer translated `={{ $json.email }}` to `=.email`. The tinyflows
expression scope root is `{item, items, run, nodes}`, so `.email` dereferences a
key that does not exist and silently resolves to null at runtime. n8n's `$json`
is the current input item, which the engine binds as `item`, so the correct
translation is `=.item.email`; `={{ $json }}` likewise becomes `=.item` rather
than `=.` (which returned the whole scope object).

Every imported workflow using `$json` bindings therefore saved cleanly, reported
no warnings, and then ran hollow — producing empty tool args. This is the exact
silent-null-binding class the domain's propose/save gates were built to prevent,
and the importer bypassed all of them.

The existing test pinned the bug (it asserted `=.email` and an empty warning
list), so it is corrected here in the same commit, along with a second test that
was relying on the buggy output. Two new tests evaluate a translated expression
against a realistic `{item, items, run, nodes}` scope and assert it resolves the
real field, keeping the old form as a negative control that resolves null.

Also warns on duplicate n8n node names (R-m6), which previously mis-wired edges
silently via a last-wins name map — every other approximation in this importer
already warns.

The fix is forward-only by design: already-imported flows keep their broken
bindings, and rewriting stored graphs cannot distinguish an importer artifact
from a hand-authored root-level jq program.
@graycyrus
graycyrus force-pushed the fix/flows-n8n-json-item-binding branch from 1a1187c to 2fe5ca7 Compare July 31, 2026 06:23
@graycyrus
graycyrus marked this pull request as ready for review July 31, 2026 09:59
@graycyrus
graycyrus requested a review from a team July 31, 2026 09:59

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

graycyrus has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/openhuman/flows/n8n_import.rs (1)

990-992: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Tighten the duplicate-id assertions; contains('a')/contains('b') are nearly tautological.

collision_warning.contains('a') and .contains('b') check for single letters, which appear in common words of the warning message (for example "named", "and", "by") regardless of whether the actual ids "a" and "b" are present. This test would still pass if the id-substitution logic silently broke. Assert the quoted id tokens instead.

🔧 Proposed fix
-        assert!(collision_warning.contains('a'));
-        assert!(collision_warning.contains('b'));
+        assert!(collision_warning.contains("'a'"));
+        assert!(collision_warning.contains("'b'"));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/flows/n8n_import.rs` around lines 990 - 992, Update the
duplicate-ID assertions in the collision warning test to verify the quoted token
values for IDs “a” and “b” rather than checking single-character containment,
ensuring the assertions fail when ID substitution is broken.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/openhuman/flows/n8n_import.rs`:
- Around line 111-129: Change the tracing call in the duplicate-name handling
around name_to_id.insert from warn to debug or trace, preserving its existing
fields and message; keep the warnings.push user-facing warning unchanged.

---

Nitpick comments:
In `@src/openhuman/flows/n8n_import.rs`:
- Around line 990-992: Update the duplicate-ID assertions in the collision
warning test to verify the quoted token values for IDs “a” and “b” rather than
checking single-character containment, ensuring the assertions fail when ID
substitution is broken.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a6880534-2fb7-496b-bbdc-f29f9e5f1de8

📥 Commits

Reviewing files that changed from the base of the PR and between 6c0381a and 2fe5ca7.

📒 Files selected for processing (1)
  • src/openhuman/flows/n8n_import.rs

Comment on lines +111 to +129
if let Some(previous_id) = name_to_id
.insert(n8n_name.clone(), Value::String(id.clone()))
.and_then(|v| v.as_str().map(str::to_string))
{
tracing::warn!(
target: "flows",
name = %n8n_name,
previous_id = %previous_id,
new_id = %id,
"[flows] n8n_import: duplicate node name collision — connections will mis-wire"
);
warnings.push(format!(
"Multiple n8n nodes are named '{n8n_name}' (ids '{previous_id}' and '{id}') — \
n8n connections are keyed by node name, so every connection naming \
'{n8n_name}' was rewired onto node '{id}' and may now point at the wrong \
node. Rename the duplicates in the source n8n workflow and re-import, or fix \
the affected edges by hand."
));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check existing tracing severity conventions in n8n_import.rs
rg -n 'tracing::(warn|error|info|debug|trace)!' src/openhuman/flows/n8n_import.rs

Repository: tinyhumansai/openhuman

Length of output: 274


🏁 Script executed:

#!/bin/bash
printf '%s\n' '--- n8n_import.rs logging contexts ---'
sed -n '65,175p' src/openhuman/flows/n8n_import.rs
printf '%s\n' '--- warning-level logging in adjacent flow modules ---'
rg -n -C 3 'tracing::warn!|tracing::error!|tracing::info!' src/openhuman/flows --glob '*.rs'

Repository: tinyhumansai/openhuman

Length of output: 50378


🏁 Script executed:

#!/bin/bash
printf '%s\n' '--- logging and PII guidance ---'
rg -n -i -C 2 'never log|full PII|PII|secrets|debug/trace|logging guideline|tracing::warn' \
  --glob '*.md' --glob '*.rs' --glob '*.toml' . \
  | head -n 240
printf '%s\n' '--- exact n8n import warning test/context ---'
sed -n '940,1020p' src/openhuman/flows/n8n_import.rs

Repository: tinyhumansai/openhuman

Length of output: 20910


Lower this log to debug or trace. The new path logs user-controlled n8n_name and node IDs. The logging guideline requires debug/trace for new or changed flows. Keep the user-facing warning.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/flows/n8n_import.rs` around lines 111 - 129, Change the tracing
call in the duplicate-name handling around name_to_id.insert from warn to debug
or trace, preserving its existing fields and message; keep the warnings.push
user-facing warning unchanged.

Source: Coding guidelines

@graycyrus
graycyrus merged commit 77a2f74 into tinyhumansai:main Jul 31, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant