Skip to content

console: an api-type action target with ${ctx.recordId} is sent uninterpolated (…/$%7Bctx.recordId%7D) — url targets interpolate, api targets do not #26722

Closed

Description

@objectstack-fleet

Filing gate: ① product defect, reproduced, landing site named.
Batch-of: studio-qa-2026-10-27 — 39 cards from one Studio QA run (coordinator session session_01J8VVAkZns5rNnK2rtszzbQ; tested at 3fcafcd3; origin/main was 43731031 at filing, so the implementer re-checks the landing site on current main). The run record is the qa-run issue filed with this batch.

Symptom

The spec documents ${ctx.recordId} for api targets, but the request goes out with the literal placeholder, so the call hits a nonexistent record.

Reproduction

  1. Action call_api on gapdata_item: type: "api", target: "/api/v1/data/gapdata_item/${ctx.recordId}", method PATCH → click on a record → network: PATCH /api/v1/data/gapdata_item/$%7Bctx.recordId%7D.
  2. Action export_tab (POST, same placeholder) → POST …/$%7Bctx.recordId%7D.
  3. Control: a type: "url" action with the same placeholder opens the interpolated URL.

Expected

api targets interpolate the same context variables as url targets.

Suspected landing site (the implementer verifies)

The console action runner for type: "api" (ui/ action executor).

Filing facts

  • reach: Console UI, measured twice by the data gap-fill card.
  • Business reason: Record-level API actions cannot be built without code; they fail on every record.
  • Reader who acts: the triage seat (first touch: domain, area, level), then that lane's os-dev implementer. QA suggestion only: domain:ui, area:records, p2.
  • Dedupe: semantic search api action target ctx.recordId not interpolated in request path on objectstack-ai/objectstack → 0 hits.
  • QA id: G6

Activity

  1. added
    bugSomething isn't working
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Oct 27, 2026
  2. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — priority:p2 · domain:ui · area:records · pm:queue (finding removed) · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-27T16:05Z

    ⛔ Not a claim, ⛔ not a dispatch.

    • p2: an api-type action target sends ${ctx.recordId} uninterpolated, while url targets interpolate. Run the same interpolation for both. ⛔ Not a second interpolator. From the studio-qa-2026-10-27 batch. Re-check the landing site on current main.

    Generated by Claude Code

  3. added
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    domain:uiDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    pm:queueReady for the PM dispatch loop
    on Oct 27, 2026
  4. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01QsR2jTfvjNTaR29ehLk26T
    Account: zhuangjianguo
    Branch: claude/project-thread-3v3vi9 (platform-assigned card-thread branch; a card thread can push only its assigned branch, and the name carries no issue number, so this claim is the identity record)
    Worktree: objectstack-issue-26722
    Domain: domain:ui
    Seat: domain:ui#4
    File surface: ui/packages/core/src/actions/ActionRunner.ts (or the api executor it calls), tests beside, one root changeset; ⛔ a second interpolator; ⛔ packages/spec/**, server packages, any new key (stop on breach; explain in the report)
    Container & model: S, mode:thread, model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate; reason: the maintainer's standing rule 「只认领不需要 fable 的卡」, 2026-10-27T10:39Z), effort: high
    Clause-②: no
    Metadata-protocol: none
    Responsibility: the console action runner (ui core, this lane) sends an api target's ${ctx.recordId} literally while url targets interpolate · the platform path that may cover it: the runner's own ${ctx.X} substitution (ActionRunner.ts around 2803), which the api path skips · reached by every record-level api action
    Thread-read: 6446976733
    Premise: holds (git grep -c 'ctx.recordId' origin/main -- ui/packages/core/src/actions/ActionRunner.ts → 1 (the substitution exists) on origin/main c8583f80 at 2026-10-27T16:51Z)
    Serial constraints cleared: no open PR touches the surface (scan of the 80 most recently updated open PRs 2026-10-27T16:51Z)

    Dispatched at 2026-10-27T17:01Z to the card thread on that branch; the order lives in the thread's opening message.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 26722,
      "status": "done",
      "branch": "claude/project-thread-3v3vi9",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/26826",
      "session": "session_01QsR2jTDpMW8KaXLg1DFU6f",
      "premise_still_valid": true,
      "summary": "Landing site measured on main c8583f80: ActionRunner.executeAPI already interpolates, but the Console registers its own api handler (useConsoleActionRuntime apiHandler via ActionProvider handlers) that replaces executeAPI and only fills {field} row tokens, so ${ctx.recordId} went out literally. ActionRunner.dispatchCollected now runs the existing interpolateTarget on an api action's string target before handing it to a registered api handler: same scopes, same encodeURIComponent as url targets, no second interpolator, no app-shell or spec change. An untokenized api action reaches the handler as the same object; other handler types get their target as authored.",
      "tests": "head 333eef54. New ActionRunner.apiHandlerTarget-26722.test.ts 7/7 (PATCH + POST resolved path, url/api same encoding, built-in executor path, url control unchanged, flow handler untouched, same-object identity). ui core: Test Files 215 passed (215), Tests 4112 passed | 27 skipped. Ablation via ablation-replace.mjs (anchor actionType === 'api' -> false, 1->0, blob restored = HEAD): 3 failed | 4 passed (PATCH, POST, encoding red; controls green). @object-ui/core type-check exit 0. ui scripts/ suite: 178 passed | 2 skipped files, 5065 tests passed.",
      "gates": "dispatch-gates --commands: 34 derived; 33 run in a loop, each exit 0; ui vitest scripts/ exit 0; --ran: 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN. PR-body gates on the body as sent: check-changeset-no-major, check-adr-0087-registration, check-closing-keyword-parity, check-partof-closing-keyword all exit 0. check:new-line-citations 0 new. eslint on changed files 0 errors. CI check-runs on PR #26826: in_progress at report time.",
      "line_budget": "151 changed lines (+148/-3), 3 files, under the 3000 threshold",
      "files_changed": [
        ".changeset/26722-api-target-interpolation.md",
        "ui/packages/core/src/actions/ActionRunner.ts",
        "ui/packages/core/src/actions/__tests__/ActionRunner.apiHandlerTarget-26722.test.ts"
      ],
      "deviations": [
        "Landing site named: ActionRunner.ts dispatchCollected registered-handler branch (the api executor the Console uses is app-shell useConsoleActionRuntime apiHandler, left unchanged).",
        "app-shell and components suites not run locally (narrowing): no ui test outside packages/core registers an api handler with a ${ token target; CI Test UI runs them.",
        "sdui manifest not regenerated: check-sdui-manifest exit 0 and the record moves only with the artefact; CI Build & E2E UI compares bytes."
      ],
      "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
      "mcp_calls": "3 — mcp__github__issue_read x3 (get, get_comments x2), read-only; plus 1 non-GitHub get_session",
      "api_writes": "2 relay strokes — pr_create (POST /repos/objectstack-ai/objectstack/pulls, POST /repos/objectstack-ai/objectstack/issues/26826/assignees), comment (POST /repos/objectstack-ai/objectstack/issues/26722/comments)",
      "open_questions": [
        {
          "question": "Should a console-level pin (register the real useConsoleActionRuntime apiHandler on an ActionRunner and assert the fetched URL) be added in app-shell?",
          "options": [
            "A: no, the runner-level pins cover the seam",
            "B: add one app-shell test (needs a surface amendment)"
          ],
          "recommendation": "A, because the fix lives in the runner and the handler is now handed an already-resolved target; B adds a ~10 min app-shell build closure for the same assertion one layer up."
        }
      ],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:uiDomain lane — seat card indexed by label:pm:seatpm:dispatchedDispatched to a dev agent by /pm-dispatchpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions