Skip to content

feat(automation): opt-in single-hop lookup expansion for record-change flow templates (#3475)#3482

Merged
os-zhuang merged 1 commit into
mainfrom
feat/3475-flow-lookup-expand
Jul 25, 2026
Merged

feat(automation): opt-in single-hop lookup expansion for record-change flow templates (#3475)#3482
os-zhuang merged 1 commit into
mainfrom
feat/3475-flow-lookup-expand

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #3475 (the lookup half of #3426). Implements the plan posted on #3475, after mapping the automation engine.

What

A record-change flow can now declare which lookup relations to expand, so node templates traverse them:

nodes: [
  { id: 'start', type: 'start', config: { objectName: 'crm_lead', triggerType: 'record-created', expand: ['crm_account'] } },
  { id: 'n1', type: 'notify', notify: { title: 'New lead from {record.crm_account.name}', body: '…' } },
]

{record.crm_account.name} now resolves instead of rendering blank.

How — expand engine-side, as the run's own identity

The key finding from scoping #3475: the triggering user's grants are resolved engine-side (resolveRunContext, #3356), and there's already a user-scoped data path (resolveRunDataContext). So expansion belongs in the engine, not the trigger:

  • AutomationEngine.setRecordExpander(fn) — new injectable, mirroring setUserGrantsResolver. In resolveRunContext, after identity resolution, the engine reads startNode.config.expand, calls the expander, and grafts the expanded relations onto the run record in place (the same object the run's variable map already references, so every per-node {record.<lookup>.<field>} resolves with no per-node change). Placed in resolveRunContext, it covers both execute() and executeWithoutRetry().
  • Plugin bridge — wires the expander to a findOne(object, { where: { id }, expand, context: resolveRunDataContext(runContext) }) on the same data engine the CRUD nodes use. resolveRunDataContext honors runAs, so:
    • runAs:'user' → the referenced object is read as the triggering user (RLS/FLS enforced) — closing the leak that made a trigger-side (system-elevated) expand unsafe. The trigger structurally can't do this: its hook session carries only userId, not resolved grants.
    • runAs:'system' → elevated, as intended.

Safety & semantics

  • Only declared relation keys are grafted. Bare lookup ids, multiple lookup arrays ([P2] record-change context does not hydrate multi-lookup (junction) fields #1872), and the formula fields the trigger already hydrated stay untouched. Formula hydration (trigger, system, own-row) and lookup expansion (engine, user-scoped, cross-object) remain separate layers — deliberately not unified, since a whole-record user-scoped re-read would FLS-mask formula fields the trigger returns unmasked.
  • Opt-in ⇒ zero cost when expand is absent. Best-effort ⇒ a re-read failure logs and leaves the record unexpanded (template renders the scalar id); expansion never breaks the flow.
  • No spec-schema / trigger / ObjectQL changeconfig is an open passthrough, findOne already supports expand, and expandRelatedRecords infers the target from the field's reference and applies the referenced object's RLS/FLS.
  • Lint consistency: flow-template-lookup-traversal (fix(#3426): build-time warning for unresolvable flow templates + guard the formula re-read #3472) is suppressed once a relation is declared in config.expand.

Testing

  • @objectstack/service-automation: 358 tests pass (33 files), including a new record-lookup-expand.integration.test.ts (4 cases): user-scoped expansion resolves {record.account.name} and the re-read carries the user context (not system); runAs:'system' reads elevated; only the declared relation is grafted (a non-declared lookup stays a scalar id, [P2] record-change context does not hydrate multi-lookup (junction) fields #1872); a re-read error is fail-safe (flow still succeeds, record unexpanded). tsc clean.
  • @objectstack/lint: 323 tests pass; the flow-template rule gains two cases (declared expand suppresses the traversal warning; a non-covered relation still warns). tsc clean.

Follow-up (optional, out of scope)

Multi-hop expansion, and a typed StartNodeConfigSchema for Studio/AI discoverability of expand (today it rides the generic passthrough config).

🤖 Generated with Claude Code

…e flow templates (#3475)

A record-change flow can declare `expand: ['<lookup>', …]` on its start-node
config so node templates resolve `{record.<lookup>.<field>}` (e.g.
`{record.account.name}` — the #3426 lookup gap).

The engine re-reads the declared relations AFTER identity resolution, as the
run's OWN principal (`resolveRunDataContext` honors `runAs`), and grafts the
expanded objects onto the run record. So a `runAs:'user'` run reads the
referenced object as the triggering USER — RLS/FLS enforced — not system-
elevated, which is exactly what made expansion unsafe in the trigger's re-read
(no resolved grants there). New `AutomationEngine.setRecordExpander`, bridged by
the plugin to the same data engine the CRUD nodes use.

Only declared keys are grafted, so bare lookup ids, `multiple` lookup arrays
(#1872), and trigger-hydrated formula fields are untouched. Opt-in ⇒ zero cost
otherwise; best-effort ⇒ a re-read failure leaves the record unexpanded and
never breaks the flow. The `flow-template-lookup-traversal` lint warning
(#3472) is suppressed once a relation is declared in `config.expand`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 25, 2026 2:25am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m labels Jul 25, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/lint, packages/services.

7 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/automation/webhooks.mdx (via packages/services)
  • content/docs/kernel/runtime-services/audit-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/index.mdx (via packages/services)
  • content/docs/kernel/runtime-services/settings-service.mdx (via packages/services)
  • content/docs/permissions/authorization.mdx (via @objectstack/lint)
  • content/docs/plugins/packages.mdx (via packages/services)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/services)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang merged commit 5524f84 into main Jul 25, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the feat/3475-flow-lookup-expand branch July 25, 2026 02:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Flow lookup-field template resolution ({record.account.name}) needs a read-identity design decision (follow-up to #3426)

1 participant