From 27ab19a4bf09753a518cdd6faab5a565cea86bb8 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Sun, 31 May 2026 22:18:06 +0800 Subject: [PATCH] feat(approvals): annotate approver value + escalateTo as typed references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `xRef` annotations to two Approval-node config strings so the Studio flow designer renders pickers instead of free text (ADR-0018 §configSchema), mirroring the existing `approvalStatusField` annotation: - `approvers[].value` — a *polymorphic* reference (`kindFrom: 'type'`): the concrete picker follows the sibling `type` column (user / role / team / department / queue), or an object-field picker resolved from the flow's `$trigger` object when `type` is `field`. `manager` and any unmapped value carry no `value` and stay free text. - `escalation.escalateTo` — a `role` reference (the common case); free text is still accepted for a specific user id. Both fold the annotation and description into a single `.meta()` so neither is dropped from the published `z.toJSONSchema` config contract. Consumed by the companion objectui PR's reference pickers. --- packages/spec/src/automation/approval.zod.ts | 30 ++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/packages/spec/src/automation/approval.zod.ts b/packages/spec/src/automation/approval.zod.ts index f161266b5..4659dc36b 100644 --- a/packages/spec/src/automation/approval.zod.ts +++ b/packages/spec/src/automation/approval.zod.ts @@ -67,7 +67,27 @@ export const ApprovalNodeApproverSchema = lazySchema(() => z.object({ * holding a user id (`field`), or queue id (`queue`). Omitted for `manager` * (resolved from the submitter's `manager_id`). */ - value: z.string().optional().describe('User id / role / team / department / field / queue — per `type`'), + // `xRef` marks this string as a *polymorphic* typed reference (ADR-0018 + // §configSchema): the concrete picker follows the sibling `type` column, so + // the Studio designer shows a user/role/team/department/queue picker — or an + // object-field picker (resolved from the flow's `$trigger` object) when + // `type` is `field`. `manager` and any unmapped value carry no `value` and + // stay free text. A single `.meta()` carries both description and annotation. + value: z.string().optional().meta({ + description: 'User id / role / team / department / field / queue — per `type`', + xRef: { + kindFrom: 'type', + objectSource: '$trigger', + map: { + user: 'user', + role: 'role', + team: 'team', + department: 'department', + field: 'object-field', + queue: 'queue', + }, + }, + }), })); export type ApprovalNodeApprover = z.infer; @@ -80,7 +100,13 @@ export const ApprovalEscalationSchema = lazySchema(() => z.object({ timeoutHours: z.number().min(1).describe('Hours before escalation triggers'), action: z.enum(['reassign', 'auto_approve', 'auto_reject', 'notify']).default('notify') .describe('Action on escalation timeout'), - escalateTo: z.string().optional().describe('User id, role, or manager level to escalate to'), + // Escalation hands the request to a role (the common case — e.g. a manager + // role or an approvals queue owner); the Studio designer renders a role + // picker, but free text is still accepted for a specific user id. + escalateTo: z.string().optional().meta({ + description: 'User id, role, or manager level to escalate to', + xRef: { kind: 'role' }, + }), notifySubmitter: z.boolean().default(true).describe('Notify the original submitter on escalation'), })); export type ApprovalEscalation = z.infer;