@opencxh/domain 1.275.0 → 1.277.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/entities/activity/types.d.ts +5 -1
  2. package/dist/entities/ai-account/modality.d.ts +0 -2
  3. package/dist/entities/ai-account/vendors.d.ts +2 -7
  4. package/dist/entities/ai-message/types.d.ts +2 -0
  5. package/dist/entities/approval/decide-each.d.ts +16 -0
  6. package/dist/entities/approval/decide-each.test.d.ts +1 -0
  7. package/dist/entities/approval/index.d.ts +1 -0
  8. package/dist/entities/business-entity/types.d.ts +16 -7
  9. package/dist/entities/catalog/pricing.d.ts +12 -1
  10. package/dist/entities/catalog/types.d.ts +5 -0
  11. package/dist/entities/custom-field-def/types.d.ts +3 -3
  12. package/dist/entities/playbook/actor.d.ts +1 -12
  13. package/dist/entities/playbook/approval.d.ts +14 -0
  14. package/dist/entities/playbook/approval.test.d.ts +1 -0
  15. package/dist/entities/playbook/autonomy.d.ts +25 -0
  16. package/dist/entities/playbook/autonomy.test.d.ts +1 -0
  17. package/dist/entities/playbook/index.d.ts +2 -0
  18. package/dist/entities/playbook/labels.d.ts +1 -3
  19. package/dist/entities/playbook/types.d.ts +30 -38
  20. package/dist/entities/reaction/types.d.ts +6 -1
  21. package/dist/entities/rule/bind.d.ts +20 -5
  22. package/dist/entities/rule/outcome.d.ts +2 -0
  23. package/dist/entities/rule/trigger-source.d.ts +4 -4
  24. package/dist/entities/rule/types.d.ts +47 -4
  25. package/dist/entities/saved-view/index.d.ts +1 -0
  26. package/dist/entities/saved-view/types.d.ts +30 -0
  27. package/dist/entities/scope/actor.d.ts +0 -5
  28. package/dist/entities/sla/types.d.ts +1 -1
  29. package/dist/entities/transaction/convert.d.ts +16 -0
  30. package/dist/entities/transaction/document.d.ts +2 -0
  31. package/dist/entities/transaction/keys.d.ts +15 -0
  32. package/dist/entities/transaction/kinds.d.ts +25 -4
  33. package/dist/entities/transaction/payment-term.d.ts +0 -8
  34. package/dist/entities/transaction/types.d.ts +21 -1
  35. package/dist/entities/webhook/types.d.ts +7 -1
  36. package/dist/entities/work/index.d.ts +2 -0
  37. package/dist/entities/work/ladder.d.ts +3 -0
  38. package/dist/entities/work/recurrence.d.ts +26 -0
  39. package/dist/entities/work/recurrence.test.d.ts +1 -0
  40. package/dist/entities/work/types.d.ts +27 -7
  41. package/dist/entities/work/view.d.ts +27 -0
  42. package/dist/index.cjs +19 -19
  43. package/dist/index.d.ts +1 -0
  44. package/dist/index.js +1796 -1488
  45. package/dist/platform/ai-tools.d.ts +34 -15
  46. package/dist/platform/permission.d.ts +2 -2
  47. package/dist/platform/resource-source.d.ts +14 -0
  48. package/dist/platform/tool-verdict.d.ts +22 -9
  49. package/package.json +1 -1
@@ -281,7 +281,7 @@ export type AIActionProposedPayload = {
281
281
  runId: string;
282
282
  playbookId?: string;
283
283
  playbookName?: string;
284
- /** The held actions, e.g. { action: "tool:mcp__jira__create_issue", params }. */
284
+ /** The held actions, e.g. { action: "mcp__jira__create_issue", params }. */
285
285
  actions: {
286
286
  action: string;
287
287
  params?: Record<string, unknown>;
@@ -289,6 +289,10 @@ export type AIActionProposedPayload = {
289
289
  status: "pending" | "approved" | "rejected";
290
290
  /** Who the proposal was made on behalf of, so the card can show "on behalf of Finance". */
291
291
  actor?: ActingIdentity;
292
+ /** The approval request that decides it — the card decides through that, not through ai. */
293
+ approvalRequestId?: string;
294
+ /** Every request this proposal waits on, one per guarded act; the first is `approvalRequestId`. */
295
+ approvalRequestIds?: string[];
292
296
  };
293
297
  /** Playbook lifecycle marker in the interaction timeline (system-authored). */
294
298
  export type PlaybookLifecyclePayload = {
@@ -22,5 +22,3 @@ export interface ModelModality {
22
22
  * declares, rather than silently refusing every attachment on a self-hosted or brand-new model.
23
23
  */
24
24
  export declare function modelAcceptsAttachment(modality: ModelModality | undefined, kind: string): boolean | undefined;
25
- /** Can this model speak? The honest question behind the `tts` flag. */
26
- export declare function modelProduces(modality: ModelModality | undefined, out: string): boolean;
@@ -1,14 +1,9 @@
1
1
  import { AiCapability } from './capabilities';
2
- /**
3
- * What a vendor can do. Was its own union of three; it is the same vocabulary the account
4
- * defaults and the usage rows use, so it now names {@link AiCapability} instead of repeating it.
5
- */
6
- export type AiVendorCapability = AiCapability;
7
2
  export interface AiVendor {
8
3
  id: string;
9
4
  label: string;
10
5
  /** What this vendor can do. Decides where it appears as an option in the UI. */
11
- capabilities: readonly AiVendorCapability[];
6
+ capabilities: readonly AiCapability[];
12
7
  /** Model a call runs on when it carries none itself. Absent for vendors without a chat adapter. */
13
8
  defaultModel?: string;
14
9
  /**
@@ -38,7 +33,7 @@ export declare function aiVendorLabel(vendorId: string): string;
38
33
  */
39
34
  export declare function aiVendorDefaultModel(vendorId: string, fallback?: string): string;
40
35
  /** Vendors offering this capability — for capability-filtered pickers. */
41
- export declare function aiVendorsWith(capability: AiVendorCapability): AiVendor[];
36
+ export declare function aiVendorsWith(capability: AiCapability): AiVendor[];
42
37
  /** True when the user has to fill in a host themselves (the vendor has no fixed one). */
43
38
  export declare function aiVendorNeedsBaseUrl(vendorId: string): boolean;
44
39
  /** Does this vendor accept this attachment kind? Text always works — that is prompt. */
@@ -39,6 +39,8 @@ export interface AIMessageOutput {
39
39
  * question and then having the same thread draft a reply is the normal flow.
40
40
  */
41
41
  export type AssistantMode = "ask" | "draft" | "agent";
42
+ /** Is this one of the three modes? For request bodies, which may carry anything. */
43
+ export declare function isAssistantMode(value: unknown): value is AssistantMode;
42
44
  export interface AIMessage {
43
45
  id: string;
44
46
  organizationId: string;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Give one verdict on every request of a proposal that this person may decide.
3
+ *
4
+ * A park asks one request per guarded act, each with its own approvers; the person pressing the
5
+ * button signs the ones that are theirs. A request that is not theirs (403) or already settled
6
+ * (400) is counted as `waiting` rather than failing the rest — anything else is a real error.
7
+ */
8
+ export declare function decideEach(requestIds: readonly string[], decideOne: (requestId: string) => Promise<unknown>): Promise<{
9
+ decided: number;
10
+ waiting: number;
11
+ }>;
12
+ /** The requests a proposal waits on, from the list or the single id older rows carry. */
13
+ export declare function proposalRequestIds(source: {
14
+ approvalRequestIds?: readonly string[] | null;
15
+ approvalRequestId?: string | null;
16
+ }): string[];
@@ -0,0 +1 @@
1
+ export {};
@@ -1,2 +1,3 @@
1
+ export * from './decide-each';
1
2
  export * from './policy';
2
3
  export * from './types';
@@ -63,12 +63,21 @@ export type BusinessEntityLike = Pick<BusinessEntity, "name" | "address" | "vatN
63
63
  /**
64
64
  * Does this row belong to this entity?
65
65
  *
66
- * Master data is owned by **exactly one** entity — two administrations with the same offering
67
- * keep two rows. An unset owner means the **default** entity, and that is what makes this cost no
68
- * backfill: every product written before entities existed keeps showing up where it always did,
69
- * and the first save stamps it for real.
70
- *
71
- * The check runs in memory because the rows are loaded whole anyway, and because "the field is
72
- * unset" is not expressible as a filter on this platform — there is no `$exists`.
66
+ * Master data is owned by **exactly one** entity. An empty owner means the **default** entity, so
67
+ * rows written before entities existed stay where they were without a backfill.
73
68
  */
74
69
  export declare function belongsToEntity(rowEntityId: string | undefined, entityId: string, isDefaultEntity: boolean): boolean;
70
+ /**
71
+ * The `businessEntityId` values that belong to this entity, as a query filter:
72
+ * `{ businessEntityId: { $in: entityOwnerIds(entity) } }`. Same rule as {@link belongsToEntity};
73
+ * it works as a filter because every sales schema defaults the column to `""`, never unset.
74
+ */
75
+ export declare function entityOwnerIds(entity: Pick<BusinessEntity, "id" | "isDefault">): string[];
76
+ /**
77
+ * A shared table with per-entity overrides: rows with an empty `businessEntityId` first, then the
78
+ * entity's own over them by `key`. How VAT rates and payment terms both work — a Belgian sister
79
+ * needs `low` at 6, not `low` hidden.
80
+ */
81
+ export declare function shadowByKey<T extends {
82
+ businessEntityId?: string;
83
+ }>(rows: readonly T[], key: keyof T, businessEntityId?: string): T[];
@@ -1,4 +1,5 @@
1
1
  import { Cents } from '../money/money';
2
+ import { TransactionLine } from '../transaction/types';
2
3
  import { PriceRule, Product } from './types';
3
4
  /**
4
5
  * The ladder: the most specific rule wins, whole. No sum, no stacking.
@@ -10,7 +11,9 @@ import { PriceRule, Product } from './types';
10
11
  * | 3 | customer + category | `customer_category` |
11
12
  * | 4 | segment + product | `segment` |
12
13
  * | 5 | segment + category | `segment_category` |
13
- * | 6 | the product's list price | `list` |
14
+ * | 6 | everyone + product | `everyone` |
15
+ * | 7 | everyone + category | `everyone_category` |
16
+ * | 8 | the product's list price | `list` |
14
17
  *
15
18
  * A tier (`minQuantity`) is not a rung of its own but a tie-break within one: of the rules that
16
19
  * pass, the highest threshold that the quantity reaches wins, and the source then reads `tier`
@@ -38,3 +41,11 @@ export interface ResolvedPrice {
38
41
  * specific its customer is: a discount on "everything" is a decision, not a price rule.
39
42
  */
40
43
  export declare function resolvePrice(product: Product, ctx: PriceContext, rules: readonly PriceRule[]): ResolvedPrice;
44
+ /** The `priceSourceKey` of a price somebody typed themselves. The ladder never overwrites it. */
45
+ export declare const MANUAL_PRICE_SOURCE = "manual";
46
+ /**
47
+ * The ladder again, for lines already on a document: after the quantity or the customer changed,
48
+ * a tier or an agreement may now apply. A typed price stays, and so does a line whose product
49
+ * is no longer in `products`.
50
+ */
51
+ export declare function repriceLines(lines: readonly TransactionLine[], products: readonly Product[], rules: readonly PriceRule[], ctx: Omit<PriceContext, "quantity">): TransactionLine[];
@@ -32,6 +32,11 @@ export interface Product {
32
32
  * types "Services" and another "services " cannot carry that.
33
33
  */
34
34
  categoryKey?: string;
35
+ /**
36
+ * Which revenue account a line books to, copied onto the line. Not `categoryKey`: that one
37
+ * groups the offering for price agreements, and "discount on services" names no account.
38
+ */
39
+ ledgerCategoryKey?: string;
35
40
  /** References to this product elsewhere (`moneybird:412`). A reference, never a copy. */
36
41
  externalIds?: string[];
37
42
  /** See {@link FIXED_UNITS}. Drives the form and the word after the quantity. */
@@ -53,9 +53,9 @@ export interface CustomFieldDef {
53
53
  */
54
54
  projectIds?: string[];
55
55
  /**
56
- * When true, this field becomes an analytics dimension `comms.cf.<key>`: every conversation-level
57
- * metric can be grouped/filtered by its value. Opt-in to avoid turning free-text fields into
58
- * high-cardinality dimensions.
56
+ * When true, the owning app reports on it: a `select` becomes a dimension (`comms.cf.<key>`,
57
+ * `work.cf.<key>`), and in work a `currency` becomes an amount summed over what closed. Opt-in
58
+ * to avoid turning free-text fields into high-cardinality dimensions.
59
59
  */
60
60
  reportable?: boolean;
61
61
  required?: boolean;
@@ -1,20 +1,9 @@
1
1
  import { ActingIdentity } from '../../platform/identity';
2
- import { toActingIdentity } from '../scope/actor';
3
2
  import { OwnerScope } from '../scope/types';
4
- /**
5
- * `ownerScope` **is** a playbook's actor: there is no separate `runAs` field. Visibility, working
6
- * area and attribution therefore all follow from the same field.
7
- *
8
- * On the storage side the personal variant is called `personal` (as with every other entity), on
9
- * the execution side `user` (as with every other acting identity). This function is that
10
- * translation, and the only place it belongs.
11
- */
12
- export declare const playbookActor: typeof toActingIdentity;
13
3
  /**
14
4
  * Who this run acts for.
15
5
  *
16
- * **Two different questions, and the whole reason this function exists next to
17
- * {@link playbookActor}:**
6
+ * **Two different questions**, which is why this is not just `toActingIdentity(ownerScope)`:
18
7
  *
19
8
  * - *who acts* — the agent, when one is designated;
20
9
  * - *who may manage this* — `ownerScope`, and that stays with humans.
@@ -0,0 +1,14 @@
1
+ /**
2
+ * A parked run's address in the approval layer: `playbook-run:<runId>~<step>`.
3
+ *
4
+ * The park is part of the key on purpose. The approval layer de-duplicates on `(scopeKey, act)`
5
+ * and keeps one approved row per key, so a flow that parks, resumes and parks again on a later
6
+ * step needs a key per park — while a retry of the *same* park must find its open request.
7
+ * `~` because the key travels in paths too, where `/` and `.` already mean something.
8
+ */
9
+ export declare const PLAYBOOK_RUN_SCOPE_KIND = "playbook-run";
10
+ /** The one act a parked run asks for: "carry out what was proposed". */
11
+ export declare const PLAYBOOK_RUN_APPROVAL_ACT = "run";
12
+ export declare function runApprovalKey(runId: string, step?: number): string;
13
+ /** The run behind an approval key's ref (`<runId>~<step>`), or the ref itself without a step. */
14
+ export declare function runIdOfApprovalRef(ref: string): string;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,25 @@
1
+ import { Autonomy } from './types';
2
+ /** Is this one of the three settings? For request bodies, which may carry anything. */
3
+ export declare function isAutonomy(value: unknown): value is Autonomy;
4
+ /**
5
+ * How much a run may do on its own, decided **once** at its start and frozen on the run. The
6
+ * playbook's setting wins where there is one (that is where the organisation configured it), then
7
+ * the agent's, then `suggest`.
8
+ *
9
+ * `demoted`: several autonomous playbooks would act outward on the same subject at once. They
10
+ * keep doing internal work themselves; a human decides which one goes out.
11
+ */
12
+ export declare function resolveAutonomy(input: {
13
+ playbook?: {
14
+ autonomy?: Autonomy;
15
+ } | null;
16
+ agent?: {
17
+ autonomy?: Autonomy;
18
+ } | null;
19
+ demoted?: boolean;
20
+ }): Autonomy;
21
+ /**
22
+ * A step's own setting may make it more careful than its run, never less: the run's value is
23
+ * where a demotion and the agent's mandate already landed.
24
+ */
25
+ export declare function narrowAutonomy(run: Autonomy | undefined, step: Autonomy | undefined): Autonomy;
@@ -0,0 +1 @@
1
+ export {};
@@ -1,5 +1,7 @@
1
1
  export * from './actor';
2
+ export * from './approval';
2
3
  export * from './assignment';
4
+ export * from './autonomy';
3
5
  export * from './labels';
4
6
  export * from './trigger-vars';
5
7
  export * from './types';
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Readable name of an action/tool for a human: `tool:communication__update_interaction` becomes
2
+ * Readable name of an action/tool for a human: `communication__update_interaction` becomes
3
3
  * `communication · update interaction`.
4
4
  *
5
5
  * Shared, because the proposal card in comms already had this helper while the run timeline in the
@@ -7,8 +7,6 @@
7
7
  * `voorstel: tool:mcp__jira__create_issue`.
8
8
  */
9
9
  export declare function humanizeAction(action: string): string;
10
- /** The namespaced tool id without the step's `tool:` prefix. */
11
- export declare function toolName(action: string): string;
12
10
  /**
13
11
  * The tool's own name, without the namespace that says who owns it:
14
12
  * `work__create_work_item` and `mcp__jira__create_issue` become `create_work_item` and
@@ -1,5 +1,7 @@
1
1
  import { ActingIdentity } from '../../platform/identity';
2
2
  import { ResourceRef } from '../../platform/resource';
3
+ import { AssignmentControl } from '../assignment/types';
4
+ import { Trigger } from '../rule/types';
3
5
  import { OwnerScope } from '../scope/types';
4
6
  /**
5
7
  * Playbooks — event-driven automation with AI steps. See docs/PLAYBOOKS.md.
@@ -7,41 +9,18 @@ import { OwnerScope } from '../scope/types';
7
9
  * Two execution forms, one contract: see {@link Decider}. Every step type in
8
10
  * {@link Step} is executed; there is no kind the executor refuses.
9
11
  */
10
- export type Autonomy = "suggest" | "auto";
11
- export type PlaybookTrigger = {
12
- kind: "activity";
13
- activityTypes: string[];
14
- /** Channel kinds: "mail" | "chat" | "voice" (derived from the activity type). */
15
- channels?: string[];
16
- /** Specific channel ids (e.g. per mailbox). Matched against activity.channelId. */
17
- channelIds?: string[];
18
- filter?: unknown;
19
- /**
20
- * Wait this many milliseconds, then run **once** for the messages in that window
21
- * instead of once per message. The run executes on the **newest** message; the ones
22
- * in between are already covered by the conversation lookup step.
23
- *
24
- * Deliberately on the trigger, not in the memory layer: coalescing is a property of
25
- * "how often do I want to react", not of what you remember.
26
- */
27
- debounceMs?: number;
28
- } | {
29
- kind: "manual";
30
- }
31
12
  /**
32
- * Someone assigns work to this playbook's agent — the wake-up call of an **assignment**.
33
- *
34
- * Not a new mechanism: an assignment is already an activity, so the same gate delivers it.
35
- * The question the engine cannot answer itself — *is this for MY agent?* — is answered by
36
- * `Playbook.agentId`, not here: one place names the agent, because that field also decides
37
- * who the run acts as. Which kinds of work can be assigned is **declared** by the app that
38
- * owns them (`assignment-source` federation, see `./assignment.ts`), hence a loose id in
39
- * `targetKind` instead of a union here.
13
+ * How much a run may do on its own: ask before every write, ask only before a write that leaves
14
+ * the organisation, or ask nothing. Ordered from most to least careful.
40
15
  */
41
- | {
42
- kind: "assignment";
43
- targetKind: string;
44
- };
16
+ export type Autonomy = "suggest" | "outward" | "auto";
17
+ /**
18
+ * The arms of {@link Trigger} the playbook executor runs: an activity, a person, an assignment.
19
+ * One vocabulary; each executor names the part it runs (rules: `event` and `schedule`).
20
+ */
21
+ export type PlaybookTrigger = Extract<Trigger, {
22
+ kind: "activity" | "manual" | "assignment";
23
+ }>;
45
24
  export interface Guardrails {
46
25
  maxSteps?: number;
47
26
  maxToolCalls?: number;
@@ -96,14 +75,14 @@ export interface ForEachStep<S = Step> {
96
75
  }
97
76
  export interface LookupStep {
98
77
  type: "lookup";
99
- /** "tool:<namespaced>" (Fase 1) or a local source ("interactions"/"tasks"). */
78
+ /** The read tool, `<app>__<name>`. */
100
79
  source: string;
101
80
  filter?: unknown;
102
81
  as: string;
103
82
  }
104
83
  export interface ActionStep {
105
84
  type: "action";
106
- /** Local action ("task.create"/"interaction.set"/"notify.user") or "tool:<namespaced>" (Fase 1). */
85
+ /** The write tool, `<app>__<name>`. */
107
86
  action: string;
108
87
  params: Record<string, unknown>;
109
88
  autonomy?: Autonomy;
@@ -120,11 +99,11 @@ export interface ActionStep {
120
99
  /**
121
100
  * Wait until this task is done. Counterpart of `wait-for-reply`, for the support-procedure
122
101
  * case "a colleague must do something: create a task and wait for it". `taskId` is an
123
- * ordinary reference, e.g. `$newTask.id` after a `create_task` with an `as`.
102
+ * ordinary reference, e.g. `{{newTask.id}}` after a `create_task` with an `as`.
124
103
  */
125
104
  export interface WaitForTaskStep {
126
105
  type: "wait-for-task";
127
- /** `$ref` or literal id of the task to wait on. */
106
+ /** `{{ref}}` or literal id of the task to wait on. */
128
107
  taskId: string;
129
108
  }
130
109
  /**
@@ -145,7 +124,7 @@ export interface ClassifyStep {
145
124
  fields?: unknown;
146
125
  instruction?: string;
147
126
  /**
148
- * Which text is classified, as a `$ref` into the run vars. Default `$trigger.text`.
127
+ * Which text is classified, as a `{{ref}}` into the run vars. Default `{{trigger.text}}`.
149
128
  * Needed when the trigger carries no text: an `INTERACTION_STATUS_CHANGED` activity has
150
129
  * only `{ fromStatus, toStatus }`, so `trigger.text` is empty. With `input: "$thread"` the
151
130
  * step reads what an earlier `lookup` fetched.
@@ -217,6 +196,8 @@ export interface ParallelStep<S = Step> {
217
196
  onBranchError?: "fail" | "continue";
218
197
  }
219
198
  export type Step = ConditionStep | ForEachStep | ParallelStep | LookupStep | ActionStep | ClassifyStep | GenerateStep | AgentStep | WaitForReplyStep | WaitForTaskStep;
199
+ /** Every step type, once. The `satisfies` pair fails to compile when a type is added or dropped. */
200
+ export declare const STEP_TYPES: readonly ["condition", "for-each", "parallel", "lookup", "action", "classify", "generate", "agent", "wait-for-reply", "wait-for-task"];
220
201
  /**
221
202
  * A **procedure**: how a scenario is handled, as prose with guardrails. The second execution
222
203
  * form next to `Step[]`; {@link AgentStep} is its inside-a-flow variant (it *contains* one).
@@ -477,6 +458,11 @@ export interface PlaybookRun {
477
458
  subjectKind?: string;
478
459
  subjectId?: string;
479
460
  status: RunStatus;
461
+ /**
462
+ * The autonomy this run started with ({@link resolveAutonomy}), frozen: a resume after an
463
+ * approval runs at the same setting it parked with. Absent on runs from before this field.
464
+ */
465
+ autonomy?: Autonomy;
480
466
  /**
481
467
  * The cause: which signal started this run, plus the input as it looked **then**.
482
468
  *
@@ -522,6 +508,12 @@ export interface PlaybookRun {
522
508
  }[] | null;
523
509
  /** The AI_ACTION_PROPOSED activity created for the current pending batch (for resolve on approve/reject). */
524
510
  proposalActivityId?: string | null;
511
+ /** The approval request a parked run waits on (`runApprovalKey`). */
512
+ approvalRequestId?: string | null;
513
+ /** Every request this park waits on, one per distinct act; resumes when all are approved. */
514
+ approvalRequestIds?: string[];
515
+ /** The move an assignment turn chose before it parked; restored when the turn resumes. */
516
+ turnControl?: AssignmentControl | null;
525
517
  /**
526
518
  * What this run waits for, when `status === "waiting"`. `null` when cleared (the store
527
519
  * writes `null`), absent on a run that is not waiting. A new wait reason costs a member in
@@ -67,9 +67,14 @@ export interface ReactionChange {
67
67
  actor: ReactionActor;
68
68
  /** `true` put it there, `false` took it away. */
69
69
  on: boolean;
70
+ /**
71
+ * The signed-in user who clicked. Absent when an app wrote it (a provider sync carrying a
72
+ * Teams emoji across), so a listener that syncs out never echoes a mirrored reaction back.
73
+ */
74
+ byUser?: string;
70
75
  }
71
76
  /**
72
- * The `reactions:changed` event, as apps receive it (`communication:reactions:changed`).
77
+ * The `reactions:changed` event, as apps receive it (`context:reactions:changed`).
73
78
  *
74
79
  * Carries **both** the resulting chip set and the delta, because the two consumers need
75
80
  * different things. A feed re-renders from `reactions` — a client that only got deltas and
@@ -1,8 +1,23 @@
1
1
  /**
2
- * Fill `{{path}}` references in action params from what the rule saw (`{ event, subject }`).
2
+ * One variable language for rules and playbooks: `{{path}}`.
3
3
  *
4
- * A param that is *only* a reference keeps the referenced value's type (`"{{subject.id}}"` → the
5
- * id, a number stays a number); a reference inside longer text is stringified. An unresolvable
6
- * path becomes `""` — the same "empty is not an instruction" the tools already read.
4
+ * A value that is *only* a reference keeps the referenced value's type (`"{{subject.id}}"` → the
5
+ * id, a number stays a number); a reference inside longer text is stringified, objects as JSON.
6
+ * `{ "var": "path" }` in a condition is json-logic's own and not handled here.
7
+ *
8
+ * What an unknown reference becomes is the executor's call:
9
+ * - `"empty"` (rules): `""` — the same "empty is not an instruction" the tools already read;
10
+ * - `"throw"` (playbooks): an error when the path's **root** is not there (a typo, or a step that
11
+ * never ran). A known root with an empty leaf stays empty: a chat message has no `subject`, and
12
+ * that is not a mistake.
7
13
  */
8
- export declare function bindParams(params: Record<string, unknown>, context: Record<string, unknown>): Record<string, unknown>;
14
+ export interface BindOptions {
15
+ onMissing?: "empty" | "throw";
16
+ /** Where the value sits, for the error message. */
17
+ where?: string;
18
+ }
19
+ export declare class UnresolvedRefError extends Error {
20
+ constructor(message: string);
21
+ }
22
+ export declare function bindParams(params: Record<string, unknown>, context: Record<string, unknown>, options?: BindOptions): Record<string, unknown>;
23
+ export declare function bindValue(value: unknown, context: Record<string, unknown>, options?: BindOptions): unknown;
@@ -19,6 +19,8 @@ export interface OutcomePrior {
19
19
  params: Record<string, unknown>;
20
20
  /** Which params were content — a field the subject owns — rather than addressing. */
21
21
  fields: string[];
22
+ /** Fields the tool reported writing with a value it chose itself (`AiToolResult.wrote`). */
23
+ wrote?: Record<string, unknown>;
22
24
  }
23
25
  /**
24
26
  * Which fields no longer hold what the action put there.
@@ -1,4 +1,5 @@
1
1
  import { ToolCatalogEntry } from '../../platform/ai-tools';
2
+ import { RuleTemplate } from './types';
2
3
  /**
3
4
  * What kind of value sits at a field, so the builder knows which widget to draw and the
4
5
  * comparison operators that make sense.
@@ -79,16 +80,15 @@ export interface EventDescriptor {
79
80
  name: string;
80
81
  /** What this event is, in the author's language: "Er komt een bericht binnen". */
81
82
  label: string;
82
- /**
83
- * What the subject is. Also the key the undo reader uses to fetch current state, so a new
84
- * subject kind needs a reader there before its runs can be undone.
85
- */
83
+ /** What the subject is, for the builder. The run itself records the subject's scope kind. */
86
84
  subjectKind: string;
87
85
  fields: EventField[];
88
86
  }
89
87
  /** What a `trigger-source` returns on `GET /provider/triggers/describe`. */
90
88
  export interface TriggerSourceDescription {
91
89
  events: EventDescriptor[];
90
+ /** Rules to start from, on this app's own events. */
91
+ templates?: RuleTemplate[];
92
92
  }
93
93
  /** An event plus the app that answered for it — the merged catalog a builder reads. */
94
94
  export interface VocabularyEvent extends EventDescriptor {
@@ -5,8 +5,9 @@ import { OutcomePrior, RunOutcome } from './outcome';
5
5
  *
6
6
  * There were five of these, and they had to agree without anything saying so: `PlaybookTrigger`,
7
7
  * `webhook.triggers[]` (four fixed strings), the sync tick, and twenty-one hand-rolled cron jobs.
8
+ * Playbooks now run a subset of this union (`PlaybookTrigger`); webhooks store `event` names.
8
9
  *
9
- * **Four arms, not five.** `stream` is deliberately absent. It existed once, as a field-less
10
+ * **No `stream` arm.** `stream` is deliberately absent. It existed once, as a field-less
10
11
  * `{ kind: "stream" }` on `PlaybookTrigger`, and was removed with a reason that generalises:
11
12
  * *"Definitions belong separated by their runtime, not by their editor."* A live read lane has no
12
13
  * run, no lock and no gate, so it is not an automation with a trigger — it is its own thing
@@ -27,18 +28,42 @@ export type Trigger =
27
28
  | {
28
29
  kind: "manual";
29
30
  }
30
- /** Work was handed to an agent. `targetKind` is declared by the app that owns that work. */
31
+ /**
32
+ * Work was handed to an agent. `targetKind` is declared by the app that owns that work
33
+ * (`assignment-source` federation, see `playbook/assignment.ts`); which agent is answered by
34
+ * `Playbook.agentId`, not here.
35
+ */
31
36
  | {
32
37
  kind: "assignment";
33
38
  targetKind: string;
39
+ }
40
+ /**
41
+ * A message appears: these activity types, optionally on these channel kinds or ids, passing
42
+ * `filter` (JSONLogic). Its own arm rather than an `event` with a filter, so the editor keeps
43
+ * its pickers instead of an expression.
44
+ */
45
+ | {
46
+ kind: "activity";
47
+ activityTypes: string[];
48
+ /** Channel kinds: "mail" | "chat" | "voice" (derived from the activity type). */
49
+ channels?: string[];
50
+ /** Specific channel ids (e.g. per mailbox). Matched against activity.channelId. */
51
+ channelIds?: string[];
52
+ filter?: unknown;
53
+ /**
54
+ * Wait this many milliseconds, then run **once** for the messages in that window instead
55
+ * of once per message, on the newest one. Deliberately on the trigger: coalescing is
56
+ * "how often do I react", not what you remember.
57
+ */
58
+ debounceMs?: number;
34
59
  };
35
60
  /**
36
61
  * One thing a rule does.
37
62
  *
38
- * **`action` is always `tool:<namespaced>`.** The action library is the tool federation — the
63
+ * **`action` is `<app>__<tool>`** — the action library is the tool federation, the
39
64
  * same 62 descriptors the assistant uses, with the model taken out from in front. A second
40
65
  * library would duplicate what a descriptor already carries: a JSON-Schema, a scope gate, an
41
- * `effect` classification and an approval hook.
66
+ * `effect` classification and an approval hook. Playbooks spell it the same.
42
67
  */
43
68
  export interface RuleAction {
44
69
  action: string;
@@ -145,3 +170,21 @@ export interface RuleRun extends RuleRunRecord {
145
170
  outcomeAt?: number;
146
171
  createdAt?: number;
147
172
  }
173
+ /**
174
+ * A rule an organisation can start from, declared by the app whose event it listens to.
175
+ *
176
+ * Data, not rows seeded on install: an organisation that never opens the builder should not
177
+ * acquire automation it did not ask for. Installing one is a normal create, so it lands disabled
178
+ * and passes the same save-time gate as anything a person writes.
179
+ */
180
+ export interface RuleTemplate {
181
+ id: string;
182
+ name: string;
183
+ description: string;
184
+ /** What the author still has to fill in. Named, so the form can ask instead of guessing. */
185
+ blanks: {
186
+ path: string;
187
+ label: string;
188
+ }[];
189
+ rule: Pick<Rule, "trigger" | "when" | "then">;
190
+ }
@@ -0,0 +1 @@
1
+ export * from './types';
@@ -0,0 +1,30 @@
1
+ import { OwnerScope } from '../scope/types';
2
+ /**
3
+ * A saved view on any list: a filter, an order, a grouping and a layout, under a name.
4
+ *
5
+ * The shape is shared, the store is not: each app keeps its own views next to the rows they
6
+ * filter (`work_view` in work), because only the owning app knows what its filters mean. The
7
+ * routes come from `savedViewRoutes` in platform-api, so an app joins with its filter shape and
8
+ * nothing else.
9
+ *
10
+ * Personal (`ownerScope.kind: "personal"`) or shared with a team or the organisation; whoever may
11
+ * write that scope may change the view. It stores what the screen chooses, never the rows.
12
+ */
13
+ export interface SavedView<TFilters extends object = Record<string, unknown>, TLayout extends string = string, TSortField extends string = string> {
14
+ id: string;
15
+ organizationId: string;
16
+ name: string;
17
+ ownerScope: OwnerScope;
18
+ filters: TFilters;
19
+ sort?: SavedViewSort<TSortField>;
20
+ /** A grouping the owning list knows; free text here, validated nowhere but there. */
21
+ groupBy?: string;
22
+ layout: TLayout;
23
+ createdBy: string;
24
+ createdAt?: number;
25
+ updatedAt?: number;
26
+ }
27
+ export interface SavedViewSort<TField extends string = string> {
28
+ field: TField;
29
+ dir: "asc" | "desc";
30
+ }
@@ -7,10 +7,5 @@ import { OwnerScope } from './types';
7
7
  * carries an id on one side and not on the other. This is that translation, and it is
8
8
  * meant to be the only one: a hand-rolled `kind === "personal" ? "user" : …` at a call
9
9
  * site is how two vocabularies for the same three rungs drift apart.
10
- *
11
- * It arrived as `playbookActor` in `entities/playbook/`, whose own docstring already
12
- * claimed to be "the only place this belongs" — correct, but it read as
13
- * playbook-specific while every scoped entity needs the same conversion. `playbookActor`
14
- * stays as a name so the playbook call sites and their tests keep working.
15
10
  */
16
11
  export declare function toActingIdentity(scope: OwnerScope): ActingIdentity;
@@ -50,7 +50,7 @@ export interface SlaTarget {
50
50
  }
51
51
  /**
52
52
  * A named set of service targets. Assigned to a resource explicitly — from a workflow rule
53
- * (`tool:context__apply_sla`) or by hand — never matched automatically. That is why there is
53
+ * (`context__apply_sla`) or by hand — never matched automatically. That is why there is
54
54
  * no `appliesTo` and no `order`: both only exist to drive a matcher nothing calls.
55
55
  */
56
56
  export interface SlaProfile {