Skip to content

feat(workflow-executor): handle polymorphic relations in load-related (skip + follow) [PRD-493]#1647

Merged
christophebrun-forest merged 1 commit into
mainfrom
fix/prd-493-executor-polymorphic-on-main
Jun 10, 2026
Merged

feat(workflow-executor): handle polymorphic relations in load-related (skip + follow) [PRD-493]#1647
christophebrun-forest merged 1 commit into
mainfrom
fix/prd-493-executor-polymorphic-on-main

Conversation

@christophebrun-forest

@christophebrun-forest christophebrun-forest commented Jun 10, 2026

Copy link
Copy Markdown
Member

What

Handle polymorphic BelongsTo relations (e.g. Rails commentableAuthor / Library / …) in the load-related-record step. These relations carry no single static target, so they were previously unfollowable. The step can now both skip them when unresolvable and follow them by resolving the concrete target per record.

Rebased onto main — supersedes #1640, which targeted the now-merged feat/prd-214-server-step-mapper and predated main's RelationCandidate refactor of the relation-selection model.

Why

A polymorphic relation has no relatedCollectionName (the orchestrator omits it). Two things were needed:

  • Skip: never offer an unfollowable relation to the AI or in the awaiting-input card.
  • Follow: for a relation that is resolvable, pick the concrete target collection per record.

The discriminator column (e.g. commentable_type) is not exposed as a Forest field, so it can't be read via a projection. The target type is instead read from the raw JSON:API linkage (data.relationships.<rel>.data = { type, id }), which the agent-client deserializer drops — then mapped to one of polymorphicReferencedModels (exact, then case-insensitive, which aligns with Rails model-name = Forest collection-name).

Changes

  • collection.ts — schema: optional polymorphicTypeField + polymorphicReferencedModels.
  • agent-port.ts / agent-client-agent-port.ts — new resolvePolymorphicType (raw linkage fetch); mintToken extracted for reuse.
  • agent-with-log.ts — unaudited passthrough (metadata probe; the actual load is audited separately).
  • load-related-record-step-executor.tsisFollowableRelation + resolveTargetCollection (static + polymorphic), wired into buildRelationCandidates (resolves per record), buildTarget, and the Branch A re-entry path.

Tests

  • Adapter: linkage read, composite id, no-linkage → null, non-2xx → error.
  • AgentWithLog: unaudited passthrough.
  • Executor: follow resolved target, case-insensitive mapping, no linkage → error, discriminator not among candidates → error, unfollowable relation not offered.

998 tests / 45 suites passing, lint clean.

Dependency

Requires the orchestrator exposing polymorphicTypeField / polymorphicReferencedModels (forestadmin-server PRD-493).

fixes PRD-493

🤖 Generated with Claude Code

Note

Handle polymorphic relations in load-related step executor skip and follow logic

  • Adds resolvePolymorphicType to the AgentPort interface and implements it in AgentClientAgentPort by fetching JSON:API linkage for a relation from the agent's by-id route, returning { type, id } or null.
  • Updates LoadRelatedRecordStepExecutor to resolve the concrete target collection for polymorphic relations per record using the new method, mapping the discriminator type to one of polymorphicReferencedModels case-insensitively.
  • Filters available relations in the UI prompt to only those with a static or resolvable polymorphic target; skips records with no linkage and errors when a discriminator doesn't match any candidate model.
  • Extends the FieldSchemaSchema zod schema with optional polymorphicTypeField and polymorphicReferencedModels fields to carry polymorphic relation metadata.
  • Behavioral Change: the step now throws a StepStateError instead of proceeding when a relation target cannot be determined.
📊 Macroscope summarized 8cd54ab. 5 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

… (skip + follow) [PRD-493]

The load-related step now handles polymorphic BelongsTo relations (e.g. Rails
`commentable`), which carry no single static target. A relation is followable
when it has a relatedCollectionName OR a polymorphicTypeField; unfollowable ones
are no longer offered to the AI or in the awaiting-input card.

For a polymorphic relation the concrete target is resolved per record: the
target type is read from the raw JSON:API linkage (data.relationships.<rel>.data
= { type, id }) — not a discriminator column, which Forest does not expose as a
field — then mapped to one of polymorphicReferencedModels (exact, then
case-insensitive). resolveTargetCollection centralises static + polymorphic
resolution and is used by buildRelationCandidates, buildTarget and the Branch A
re-entry path.

Depends on the orchestrator exposing polymorphicTypeField /
polymorphicReferencedModels (forestadmin-server PRD-493).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@linear-code

linear-code Bot commented Jun 10, 2026

Copy link
Copy Markdown

PRD-493

@qltysh

qltysh Bot commented Jun 10, 2026

Copy link
Copy Markdown

2 new issues

Tool Category Rule Count
qlty Structure Deeply nested control flow (level = 4) 1
qlty Structure Function with many returns (count = 4): resolveTargetCollection 1


const body = (await response.json()) as {
data?: { relationships?: Record<string, { data?: { type?: string; id?: string } | null }> };
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium adapters/agent-client-agent-port.ts:276

resolvePolymorphicType returns { type, id: "undefined" } when the agent returns a linkage with type but no id (e.g., { type: 'orders' }). String(linkage.id) produces the string "undefined" rather than null, unlike getSingleRelatedData which guards against missing id. Downstream fetches will fail when trying to retrieve record "undefined". Consider adding the same guard: return null unless both type and id are present.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/workflow-executor/src/adapters/agent-client-agent-port.ts around line 276:

`resolvePolymorphicType` returns `{ type, id: "undefined" }` when the agent returns a linkage with `type` but no `id` (e.g., `{ type: 'orders' }`). `String(linkage.id)` produces the string `"undefined"` rather than `null`, unlike `getSingleRelatedData` which guards against missing `id`. Downstream fetches will fail when trying to retrieve record `"undefined"`. Consider adding the same guard: return `null` unless both `type` and `id` are present.

Evidence trail:
packages/workflow-executor/src/adapters/agent-client-agent-port.ts lines 274-279 (resolvePolymorphicType: no guard on linkage.id), lines 178-180 (getSingleRelatedData: guards with `if (!linkage || !packedId) return null`). JavaScript spec: String(undefined) === "undefined".

@qltysh

qltysh Bot commented Jun 10, 2026

Copy link
Copy Markdown

Qlty


Coverage Impact

This PR will not change total coverage.

Modified Files with Diff Coverage (3)

RatingFile% DiffUncovered Line #s
Coverage rating: A Coverage rating: A
...ow-executor/src/executors/load-related-record-step-executor.ts100.0%
Coverage rating: A Coverage rating: A
...ages/workflow-executor/src/adapters/agent-client-agent-port.ts100.0%
Coverage rating: A Coverage rating: A
packages/workflow-executor/src/executors/agent-with-log.ts100.0%
Total100.0%
🚦 See full report on Qlty Cloud »

🛟 Help
  • Diff Coverage: Coverage for added or modified lines of code (excludes deleted files). Learn more.

  • Total Coverage: Coverage for the whole repository, calculated as the sum of all File Coverage. Learn more.

  • File Coverage: Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)

    • Indirect Changes: Changes to File Coverage for files that were not modified in this PR. Learn more.

@matthv matthv left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

ok for me

@christophebrun-forest christophebrun-forest merged commit f7eef97 into main Jun 10, 2026
30 checks passed
@christophebrun-forest christophebrun-forest deleted the fix/prd-493-executor-polymorphic-on-main branch June 10, 2026 14:18
forest-bot added a commit that referenced this pull request Jun 10, 2026
# @forestadmin/workflow-executor [1.1.0](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/workflow-executor@1.0.1...@forestadmin/workflow-executor@1.1.0) (2026-06-10)

### Features

* **workflow-executor:** handle polymorphic relations in load-related (skip + follow) [PRD-493] ([#1647](#1647)) ([f7eef97](f7eef97))
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.

2 participants