From dcd4a83687bac27383fbc6d51631cb7be0b4c39a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 07:59:55 +0000 Subject: [PATCH 1/3] docs(skills): say which hook doors provide ctx.title The sandbox ctx table listed ctx.title with no qualifier, which read as a member of every hook context. State it per door: a body has it; a deprecated handler gets it at runtime but HookContext does not declare it; ql.registerHook and a bare ObjectQL bindHooks do not provide it. Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude --- skills/objectstack-data/references/data-hooks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/objectstack-data/references/data-hooks.md b/skills/objectstack-data/references/data-hooks.md index c0625c1b1f5..35be73cb414 100644 --- a/skills/objectstack-data/references/data-hooks.md +++ b/skills/objectstack-data/references/data-hooks.md @@ -366,7 +366,7 @@ The sandbox is handed a **JSON snapshot** of these (built by | `ctx.api` | object | Cross-object CRUD. Gated by `api.read` / `api.write` — see below. | | `ctx.log` | `{ debug, info, warn, error }` | Gated by `log`. Call **`ctx.log.info(msg, data?)`** — `ctx.log` is an **object, not** callable as `ctx.log(msg)`. Emission is **best-effort** (see Troubleshooting). | | `ctx.crypto` | `{ randomUUID }` | Gated by `crypto.uuid`. | -| `ctx.title` | `(field?) => Promise` | **Name the record instead of printing its id.** `await ctx.title()` resolves this object's `nameField` — including when it is a **formula**, evaluated server-side, with no extra read. `await ctx.title('account_id')` resolves the related record's title through a lookup column (one `findOne`, gated by `api.read`; the no-argument form needs no capability). `null` when there is no title — it never falls back to the id. | +| `ctx.title` | `(field?) => Promise` | **Name the record instead of printing its id.** `await ctx.title()` resolves this object's `nameField` — including when it is a **formula**, evaluated server-side, with no extra read. `await ctx.title('account_id')` resolves the related record's title through a lookup column (one `findOne`, gated by `api.read`; the no-argument form needs no capability). `null` when there is no title — it never falls back to the id. **Not on every door:** a `body` has it (the recommended form); a deprecated `handler` gets it at runtime but `HookContext` does not declare it (an annotated handler fails to compile) — use `body`; `ql.registerHook` and a bare `ObjectQL` `bindHooks` do not provide it. | (Action bodies additionally receive `ctx.recordId` and `ctx.record`, and their wrap is `(async (input, ctx) => { … })(input, ctx)` — the action params arrive as From eee9ae8ded646490ebcd3de1031e3e7f8b0abebb Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 08:01:23 +0000 Subject: [PATCH 2/3] docs(changeset): patch @objectstack/skills for the ctx.title door note Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude --- .changeset/24289-hook-ctx-title-doors.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changeset/24289-hook-ctx-title-doors.md diff --git a/.changeset/24289-hook-ctx-title-doors.md b/.changeset/24289-hook-ctx-title-doors.md new file mode 100644 index 00000000000..561aee21325 --- /dev/null +++ b/.changeset/24289-hook-ctx-title-doors.md @@ -0,0 +1,9 @@ +--- +'@objectstack/skills': patch +--- + +docs(skills): `objectstack-data` says which hook doors provide `ctx.title` — a `body` has it, a deprecated `handler` gets it at runtime without `HookContext` declaring it, and `ql.registerHook` and a bare `ObjectQL` `bindHooks` do not provide it (#24289) + +Clause-②: no + +`skills/objectstack-data/references/data-hooks.md` listed `ctx.title(field?)` in the sandbox `ctx` table with no qualifier, so it read as a member of every hook context. It is not: a sandboxed `body` has it (the recommended form); an inline or named `handler`, which is deprecated, is handed it at runtime when the runtime binds the stack's hooks, but `HookContext` does not declare it, so a handler annotated with that type fails to compile; a hook registered with `ql.registerHook(...)`, or bound through `ObjectQL.bindHooks` with no runtime, gets no `ctx.title` at all. The row now says so. Text only; nothing an author writes changes, and no hook context changes. From 953159abed7704c614f13f86882241d71ffb9e6d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 08:11:34 +0000 Subject: [PATCH 3/3] docs(skills): pay the ctx.title door note out of the same row The skills token ratchet caps data-hooks.md; the door note is paid for by deleting the row's duplicate of the capability table (the no-argument form needs no capability) and its wordier phrasing. Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude --- skills/objectstack-data/references/data-hooks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/objectstack-data/references/data-hooks.md b/skills/objectstack-data/references/data-hooks.md index 35be73cb414..54a04ce8d58 100644 --- a/skills/objectstack-data/references/data-hooks.md +++ b/skills/objectstack-data/references/data-hooks.md @@ -366,7 +366,7 @@ The sandbox is handed a **JSON snapshot** of these (built by | `ctx.api` | object | Cross-object CRUD. Gated by `api.read` / `api.write` — see below. | | `ctx.log` | `{ debug, info, warn, error }` | Gated by `log`. Call **`ctx.log.info(msg, data?)`** — `ctx.log` is an **object, not** callable as `ctx.log(msg)`. Emission is **best-effort** (see Troubleshooting). | | `ctx.crypto` | `{ randomUUID }` | Gated by `crypto.uuid`. | -| `ctx.title` | `(field?) => Promise` | **Name the record instead of printing its id.** `await ctx.title()` resolves this object's `nameField` — including when it is a **formula**, evaluated server-side, with no extra read. `await ctx.title('account_id')` resolves the related record's title through a lookup column (one `findOne`, gated by `api.read`; the no-argument form needs no capability). `null` when there is no title — it never falls back to the id. **Not on every door:** a `body` has it (the recommended form); a deprecated `handler` gets it at runtime but `HookContext` does not declare it (an annotated handler fails to compile) — use `body`; `ql.registerHook` and a bare `ObjectQL` `bindHooks` do not provide it. | +| `ctx.title` | `(field?) => Promise` | **Name the record, not its id.** `ctx.title()` resolves this object's `nameField`, a **formula** included (no extra read); `ctx.title('account_id')` resolves a lookup's title (needs `api.read`). `null` when there is no title, never the id. **Per door:** `body` has it; a deprecated `handler` gets it at runtime, undeclared on `HookContext` (TS2339 when annotated) — use `body`; `ql.registerHook` and a bare `ObjectQL` `bindHooks` lack it. | (Action bodies additionally receive `ctx.recordId` and `ctx.record`, and their wrap is `(async (input, ctx) => { … })(input, ctx)` — the action params arrive as