Skip to content

Commit 75b47a8

Browse files
os-zhuangclaude
andauthored
docs: align product docs with framework 15.x→16.0 changes (#52)
* docs(build/data): align data-modeling pages with framework 16.0 - enable.softDelete → enable.trash in the capability example (softDelete is a 16.0 tombstone; object.zod.ts throws with guidance); added a 'Changed in 16.0' callout covering the removed object props (versioning, search→searchableFields, recordName→autonumber nameField, keyPrefix, tags, active, abstract). - referenceFilters warning upgraded from 'accepted but unread' to removed-in-16.0 (stripped at parse; lookupFilters + dependsOn is the only form). - summaryOperations example gains the new 16.0 filter (filtered roll-ups, #1868) with re-aggregation semantics. - Validation rules: 'delete' event removed (guard deletions with beforeDelete — the evaluator never ran on delete); state_machine gains initialStates (#3165) with the events:['insert','update'] requirement called out (events gates dispatch, so insert must be listed or initialStates is dead); noted 16.0 evaluates rules on multi-row updates (#3106). - Formulas: 'New in 16.0' — date-vs-number arithmetic is a build-time error pointing at daysBetween/daysFromNow/addDays/addMonths (#3306); null-guard ternary compiles; floor/ceil registered; date equality now matches via temporal coercion (#3183). - zh-Hans twins updated faithfully; other locale twins' code blocks fixed mechanically. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Kj2MJEPXoUXC4LiC3XYJc * docs(reference): align field-types and env-vars with framework 16.0 field-types (all 7 locales): - referenceFilters example → structured lookupFilters [{ field, operator, value }] with the operator enum noted (removed #2377/ADR-0049); - fileAttachmentConfig block → flat multiple/accept/maxSize (nested config removed); - vector row → flat dimensions only (distanceMetric/indexed/indexType removed); - summary row gains the 16.0 summaryOperations.filter (#1868); - removed the columnName and field-level index rows (column always equals field name / indexes live on object indexes[]), with one migration note. environment-variables (all 7 locales): - OS_MCP_SERVER_ENABLED row rewritten: ON by default since 16.0, governs only the HTTP /api/v1/mcp surface, explicit false/0/off/no disables (types/src/env.ts); notes the one-release deprecated =true stdio auto-start; - added OS_MCP_STDIO_ENABLED (own switch, default off) and OS_MCP_STDIO_API_KEY (osk_... principal, same verify+authz chain as HTTP, fail-closed boot refusal — ADR-0101). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Kj2MJEPXoUXC4LiC3XYJc * docs(interface+automation): align with framework 16.0 strict widgets, approvals, time-relative triggers pages + dashboards (en + zh-Hans): - widget examples rewritten from the pre-ADR-0021 inline-analytics shape (object/categoryField/valueField/aggregate) onto the dataset shape (dataset + dimensions + values) — DashboardWidgetSchema is .strict() since 16.0 and the legacy keys are parse errors naming the key (#3251); the dashboards note now states that instead of 'silently stripped'. approvals (en + zh-Hans): - approver-type warning aligned with #3133: canonical org_membership_level, role = one-release deprecated alias, position names → type:'position'; real lint ids; - new 16.0 section: behavior first_response|unanimous|quorum|per_group with minApprovals (runtime-clamped, never deadlocks), approver group labels (会签), open-time snapshot + OOO substitution, decision file attachments, server-computed decision_progress, and the declared-actions inbox model (approve/reject/reassign/send-back/ request-info/remind/recall/resubmit as type:'api' actions on sys_approval_request). flows + workflows (en + zh-Hans): - new time-relative trigger section: start-node config.timeRelative (object, dateField, offsetDays xor withinDays, optional filter, maxRecords 1000, default daily 08:00 UTC schedule) with a defineFlow example and os validate readiness notes (#1874); - workflows migration table splits 'N days before a date' (time- relative) from fixed-clock scheduled flows and warns off the date-equality-on-record-change pattern; notes date equality now matches for same-day checks (#3183). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Kj2MJEPXoUXC4LiC3XYJc * docs(configure+changelog): align MCP, batch, and user-import docs with 16.0; add 16.0 changelog entry mcp (en + zh-Hans): - new 'The stdio transport (16.0)' section: OS_MCP_STDIO_ENABLED own switch (default off; legacy OS_MCP_SERVER_ENABLED=true trigger for one release with a deprecation warning) and the mandatory OS_MCP_STDIO_API_KEY principal — same verify+authz chain as HTTP, RLS/FLS/tenant apply, revocation live, fail-closed boot, no system bypass (ADR-0101); - tool table gains validate_expression (read-only, data:read-scoped, fail-closed on sys_*); dev-banner claude mcp add note; new troubleshooting row. api-access (all 7 locales): - MCP row reflects default-on (disable with =false); - /api/v1/batch documented as atomic-only with per-op gating before the transaction, ids required, {$ref} resolution enforced, atomic:false → 400 BATCH_NOT_ATOMIC (#1604); discovery advertises capabilities.transactionalBatch (#3298). users + authentication (en + zh-Hans): - import-users default passwordPolicy is 'auto' since 16.0 (invite deliverable rows; temp password only for unreachable; pass 'none' for the old identity-only behavior; rows[].delivery outcomes). changelog (en + zh-Hans): - new 16.0 entry in the page's own style (organizationId rename, approvals quorum/会签 + metadata-driven inbox, time-relative automations, filtered roll-ups, strict widgets, MCP stdio principal, enforce-or-remove sweep, engine-owned read-only rows, date-equality fix + date-arithmetic build errors, bulk-import auto policy); 14.x entry no longer claims to be the current train. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Kj2MJEPXoUXC4LiC3XYJc --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 70e111f commit 75b47a8

52 files changed

Lines changed: 719 additions & 158 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

content/docs/build/automation/approvals.mdx

Lines changed: 73 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,65 @@ The node declares **who approves** and how their decisions aggregate:
5757
| Config | What it does |
5858
|---|---|
5959
| `approvers` | Who must decide — a named user, a position, or a user resolved from a record field (e.g. the submitter's manager) |
60-
| `behavior` | How multiple approvers aggregate, e.g. `'unanimous'` |
60+
| `behavior` | How multiple approvers aggregate: `'first_response'` (default), `'unanimous'`, `'quorum'`, `'per_group'` |
61+
| `minApprovals` | Approvals required — total for `quorum` (M of N), per group for `per_group` (default 1) |
6162
| `approvalStatusField` | Optional record field the plugin mirrors the request status into |
6263
| `lockRecord` | Lock the record against edits while the request is pending |
6364

64-
> **Warning — `position` vs `role`.** `{ type: 'position', value: 'finance_manager' }`
65-
> routes to the holders of a position (`sys_user_position`). The `role` approver
66-
> type is the org-membership tier (`sys_member.role`: `owner`/`admin`/`member`) —
67-
> a position name authored as `type: 'role'` matches nobody and the request
68-
> stalls. `os lint` flags this (`approval-role-not-membership-tier`).
65+
> **Warning — `position` vs the membership tier.**
66+
> `{ type: 'position', value: 'finance_manager' }` routes to the holders of a
67+
> position (`sys_user_position`). The org-membership tier (`sys_member.role`:
68+
> `owner`/`admin`/`member`) is addressed as `type: 'org_membership_level'`
69+
> since 16.0; the old spelling `role` is a **deprecated alias for one
70+
> release** — it still loads and resolves identically, with a warning, and is
71+
> removed in the next major. A position name authored as the membership-tier
72+
> type matches nobody and the request stalls. `os lint` flags both cases
73+
> (`approval-approver-not-membership-tier`,
74+
> `approval-approver-type-deprecated`); if the value names an org position,
75+
> the fix is `type: 'position'`.
76+
77+
### Quorum and per-group sign-off (16.0)
78+
79+
Beyond `first_response` and `unanimous`, 16.0 adds two aggregation modes.
80+
**Quorum** finalizes on M-of-N approvals:
81+
82+
```ts
83+
config: {
84+
approvers: [
85+
{ type: 'user', value: 'director_a' },
86+
{ type: 'user', value: 'director_b' },
87+
{ type: 'user', value: 'director_c' },
88+
],
89+
behavior: 'quorum',
90+
minApprovals: 2, // any 2 of the 3 approve
91+
}
92+
```
93+
94+
**Per-group** requires a sign-off from *each* labeled group (会签) — label
95+
approvers with `group`, and the node advances only once every group has
96+
`minApprovals` approvals (default 1 per group):
97+
98+
```ts
99+
config: {
100+
approvers: [
101+
{ type: 'position', value: 'legal_counsel', group: 'legal' },
102+
{ type: 'position', value: 'finance_manager', group: 'finance' },
103+
],
104+
behavior: 'per_group', // one sign-off from legal AND one from finance
105+
}
106+
```
107+
108+
Semantics that hold across every mode:
109+
110+
- Approver sets are tallied against an **open-time snapshot** with
111+
out-of-office substitution — who must respond is fixed when the request
112+
opens, and OOO approvers are substituted rather than stalling it.
113+
- A **single rejection is still a veto**: it finalizes the node as
114+
`rejected` in every mode.
115+
- Thresholds **clamp** at runtime to the resolvable approver count, so a
116+
misconfigured `minApprovals` can never deadlock a request.
117+
- Approvers without a `group` label each form their own group, so a plain
118+
approver list still behaves sensibly under `per_group`.
69119

70120
## A complete approval flow
71121

@@ -133,7 +183,23 @@ While a request is pending:
133183
3. If you set `approvalStatusField`, the record's own field mirrors the request
134184
status, so views and reports can filter on it.
135185
4. The approver approves or rejects; the plugin resumes the paused flow down
136-
the matching edge.
186+
the matching edge. Since 16.0 a decision can carry **file attachments**
187+
(persisted on `sys_approval_action.attachments`, threaded through the
188+
decide/comment routes and the client SDK) — signed PDFs and evidence live
189+
with the decision.
190+
5. The request exposes a server-computed **`decision_progress`**
191+
approvals got vs. needed for `unanimous`/`quorum`, a per-group breakdown
192+
for `per_group` — so the inbox and your own UIs render progress without
193+
re-implementing the tally.
194+
195+
**Decisions are declared actions (16.0).** The full decision set — approve,
196+
reject, reassign, send-back for revision (`/revise`), request-info, remind,
197+
recall, resubmit — is declared as `type: 'api'` actions on
198+
`sys_approval_request`, with typed params, pending-only visibility gates, and
199+
submitter-vs-approver predicates (e.g. `record.submitter_id == ctx.user.id`).
200+
The Console approvals inbox renders those declared actions instead of
201+
hand-written buttons, so new decision capabilities ship as metadata — no
202+
client release required.
137203

138204
Approving is itself a gated action. Model "may approve" as a capability (e.g.
139205
`approve_invoice`) granted by the approver's permission set, and gate the

content/docs/build/automation/approvals.zh-Hans.mdx

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,47 @@ description: 把记录路由给人签核 —— 同时不让自动化悄悄绕
5151
| 配置 | 作用 |
5252
|---|---|
5353
| `approvers` | 谁必须决策 —— 具名用户、岗位,或从记录字段解析出的用户(如提交人的经理) |
54-
| `behavior` | 多个审批人如何聚合,如 `'unanimous'`(一致同意) |
54+
| `behavior` | 多个审批人如何聚合:`'first_response'`(默认)、`'unanimous'``'quorum'``'per_group'` |
55+
| `minApprovals` | 所需批准数 —— `quorum` 为总数(N 中取 M),`per_group` 为每组所需数(默认 1) |
5556
| `approvalStatusField` | 可选的记录字段,插件把请求状态镜像进去 |
5657
| `lockRecord` | 请求待定期间锁定记录、禁止编辑 |
5758

58-
> **警告 —— `position` vs `role`**`{ type: 'position', value: 'finance_manager' }` 路由给某个岗位的持有者(`sys_user_position`)。而 `role` 审批人类型是组织成员层级(`sys_member.role``owner`/`admin`/`member`)—— 把岗位名写成 `type: 'role'` 谁也匹配不上,请求就会卡住。`os lint` 会标记这个问题(`approval-role-not-membership-tier`)。
59+
> **警告 —— `position` 与成员层级。**`{ type: 'position', value: 'finance_manager' }` 路由给某个岗位的持有者(`sys_user_position`)。组织成员层级(`sys_member.role``owner`/`admin`/`member`)自 16.0 起写作 `type: 'org_membership_level'`;旧写法 `role`**保留一个版本的弃用别名** —— 仍能加载并按相同方式解析,但会给出警告,下个大版本移除。把岗位名写成成员层级类型谁也匹配不上,请求就会卡住。`os lint` 对两种情况都会标记(`approval-approver-not-membership-tier``approval-approver-type-deprecated`);如果值是岗位名,正确写法是 `type: 'position'`
60+
61+
### 法定人数与会签(16.0)
62+
63+
`first_response``unanimous` 之外,16.0 新增两种聚合模式。**Quorum(法定人数)**在达到 N 中取 M 个批准时定案:
64+
65+
```ts
66+
config: {
67+
approvers: [
68+
{ type: 'user', value: 'director_a' },
69+
{ type: 'user', value: 'director_b' },
70+
{ type: 'user', value: 'director_c' },
71+
],
72+
behavior: 'quorum',
73+
minApprovals: 2, // 3 人中任意 2 人批准
74+
}
75+
```
76+
77+
**Per-group(会签)**要求*每个*带标签的组都签核 —— 用 `group` 给审批人打标签,只有当每个组都达到 `minApprovals` 个批准(默认每组 1 个)时节点才会前进:
78+
79+
```ts
80+
config: {
81+
approvers: [
82+
{ type: 'position', value: 'legal_counsel', group: 'legal' },
83+
{ type: 'position', value: 'finance_manager', group: 'finance' },
84+
],
85+
behavior: 'per_group', // 法务一个签核,且财务一个签核
86+
}
87+
```
88+
89+
对所有模式都成立的语义:
90+
91+
- 审批人集合按**开启时快照**统计,并带休假(OOO)替补 —— 谁必须响应在请求开启时就已确定,休假的审批人会被替补而不是卡住请求。
92+
- **单个拒绝仍然是一票否决**:在任何模式下它都把节点定案为 `rejected`
93+
- 阈值在运行时**收拢(clamp)**到可解析的审批人数量,因此配置错误的 `minApprovals` 永远不会让请求死锁。
94+
- 没有 `group` 标签的审批人各自成组,所以普通的审批人列表在 `per_group` 下依然行为合理。
5995

6096
## 一个完整的审批流程
6197

@@ -114,7 +150,10 @@ export const opportunityApproval = defineFlow({
114150
1. 插件持久化请求(`sys_approval_request`)以及针对它的每个决策(`sys_approval_action`)—— 你的审批历史是可查询的数据。
115151
2. 设置 `lockRecord: true` 时,记录被锁定、禁止编辑,直到决策落地。
116152
3. 如果设置了 `approvalStatusField`,记录自身的字段会镜像请求状态,视图和报表就能按它筛选。
117-
4. 审批人批准或拒绝;插件沿匹配的边恢复被暂停的流程。
153+
4. 审批人批准或拒绝;插件沿匹配的边恢复被暂停的流程。自 16.0 起,决策可以携带**文件附件**(持久化在 `sys_approval_action.attachments`,贯通决策/评论路由与客户端 SDK)—— 签署的 PDF 和证据材料与决策存放在一起。
154+
5. 请求暴露服务端计算的 **`decision_progress`** —— `unanimous`/`quorum` 为已获批准数 vs. 所需数,`per_group` 为按组明细 —— 收件箱和你自己的 UI 直接渲染进度,无需重新实现计票。
155+
156+
**决策即声明式操作(16.0)。**完整的决策集合 —— 批准、拒绝、转办、退回修改(`/revise`)、补充材料、催办、撤回、重新提交 —— 以 `type: 'api'` 操作的形式声明在 `sys_approval_request` 上,带类型化参数、仅待定时可见的门控,以及提交人 vs 审批人谓词(如 `record.submitter_id == ctx.user.id`)。Console 审批收件箱渲染的是这些声明的操作,而不是手写按钮,因此新的决策能力以元数据形式发布 —— 无需客户端发版。
118157

119158
批准本身也是被门控的操作。把"可以批准"建模为一个能力(如 `approve_invoice`),由审批人的权限集授予,并把批准操作的 `requiredPermissions` 建立在它之上 —— 这样门控就在 **UI 和服务端**两侧同时强制,而不只是从屏幕上藏起来。
120159

content/docs/build/automation/flows.mdx

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,13 @@ export default defineStack({
2626
});
2727
```
2828

29-
## Three flow types
29+
## Flow types
3030

3131
| Type | Triggered by | Use for |
3232
|---|---|---|
3333
| **Autolaunched** | A record change (insert/update/delete) | "Send welcome email when user registers" |
3434
| **Scheduled** | Cron expression or interval | "Mark stale tasks every night at 2am" |
35+
| **Time-relative** *(16.0)* | A date field's proximity to today, via a daily sweep | "Remind me 60 days before the contract ends" |
3536
| **Manual** | User clicks a button in Console, or an API call | "Approve invoice" actions |
3637

3738
## Autolaunched: react to a record change
@@ -107,6 +108,80 @@ export const nightlyCleanup = defineFlow({
107108
Backed by the `@objectstack/service-job` capability — see
108109
[Runtime Capabilities](/docs/reference/runtime-capabilities).
109110

111+
## Time-relative: fire relative to a date field (16.0)
112+
113+
"Remind me 60 days before the contract ends" used to be authored as a
114+
record-change flow gated on `record.end_date == daysFromNow(60)` — a
115+
predicate that is only evaluated when the record *happens to change*, so
116+
unattended it fires almost never. Don't write that. Declare a
117+
**time-relative trigger** instead: the flow's start node carries a
118+
`config.timeRelative` descriptor, and the runtime sweeps the object on a
119+
schedule and launches the flow **once per matching record**:
120+
121+
```ts
122+
export const renewalReminder = defineFlow({
123+
name: 'contract_renewal_reminder',
124+
type: 'schedule',
125+
status: 'active',
126+
nodes: [
127+
{
128+
id: 'start',
129+
type: 'start',
130+
config: {
131+
timeRelative: {
132+
object: 'contracts',
133+
dateField: 'end_date',
134+
offsetDays: [60, 30, 7], // T-minus thresholds
135+
filter: { status: 'active' }, // AND-ed with the date window
136+
},
137+
// Optional sweep cadence — defaults to daily at 08:00 UTC.
138+
schedule: { type: 'cron', expression: '0 8 * * *' },
139+
},
140+
},
141+
{ id: 'notify_owner', type: 'notify', label: 'Notify Owner' },
142+
{ id: 'end', type: 'end' },
143+
],
144+
edges: [
145+
{ id: 'e1', source: 'start', target: 'notify_owner' },
146+
{ id: 'e2', source: 'notify_owner', target: 'end' },
147+
],
148+
});
149+
```
150+
151+
| Key | What it declares |
152+
|---|---|
153+
| `object` | Object (machine name) whose records the sweep queries |
154+
| `dateField` | `date` / `datetime` field evaluated relative to today (day-granular) |
155+
| `offsetDays` | **Offset mode** — fire when `dateField` is exactly today + each listed offset (`[60, 30, 7]`; negative = past, e.g. `[-1]` the day after) |
156+
| `withinDays` | **Range mode** — fire every day `dateField` is within N days of today: positive = upcoming ("expiring soon"), negative = bounded overdue lookback, `0` = due today |
157+
| `filter` | Optional ObjectQL where-map AND-ed with the computed date window (e.g. `{ status: 'active' }`) |
158+
| `maxRecords` | Cap on records launched per sweep (default 1000; the sweep logs when it clamps) |
159+
160+
Exactly **one** of `offsetDays` or `withinDays` must be set. The two other
161+
common shapes:
162+
163+
```ts
164+
// "Expiring soon" — fires every day a document is within 30 days of expiry.
165+
timeRelative: { object: 'hr_document', dateField: 'expires_on', withinDays: 30 }
166+
167+
// Overdue sweep — fires for POs up to 14 days past due (bounded lookback).
168+
timeRelative: { object: 'purchase_order', dateField: 'due_date',
169+
withinDays: -14, filter: { status: 'open' } }
170+
```
171+
172+
Each launch puts the matching record on the automation context, so the
173+
start-node `condition` and `{record.<field>}` interpolation work exactly
174+
as they do for record-change flows. The discovery query runs as system,
175+
with per-record failure isolation. `os validate` gains readiness checks —
176+
it warns when `timeRelative.object` names an object the stack doesn't
177+
define, and when an auto-triggered flow is left in `draft` status.
178+
179+
> **Genuine same-day checks are fine now (#3183).** In 16.0
180+
> `record.due_date == today()` matches — the engine coerces temporal
181+
> `==` / `!=` comparisons so a date field compares correctly against
182+
> `today()`. Use that for a real "due today" condition; use
183+
> `timeRelative` for anything of the form "N days before/after a date".
184+
110185
## Manual: actions and approvals
111186

112187
```ts

content/docs/build/automation/flows.zh-Hans.mdx

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@ export default defineStack({
2020
});
2121
```
2222

23-
## 三种流程类型
23+
## 流程类型
2424

2525
| 类型 | 触发方式 | 用于 |
2626
|---|---|---|
2727
| **Autolaunched** | 记录变更(insert/update/delete) | "用户注册时发欢迎邮件" |
2828
| **Scheduled** | Cron 表达式或间隔 | "每晚 2 点标记过期任务" |
29+
| **Time-relative** *(16.0)* | 日期字段与今天的距离,经由每日扫描 | "合同到期前 60 天提醒我" |
2930
| **Manual** | 用户在 Console 点按钮,或 API 调用 | "审批发票"等 Action |
3031

3132
## Autolaunched:对记录变更做出反应
@@ -96,6 +97,64 @@ export const nightlyCleanup = defineFlow({
9697

9798
`@objectstack/service-job` 能力承载 —— 见 [Runtime Capabilities](/docs/reference/runtime-capabilities)
9899

100+
## Time-relative:相对日期字段触发(16.0)
101+
102+
"合同到期前 60 天提醒我"以前只能写成一个用 `record.end_date == daysFromNow(60)` 做门控的记录变更流程 —— 这个谓词只有在记录*恰好被修改*时才会求值,无人照看时几乎永远不会触发。别这么写。改为声明**时间相对触发器**:流程的开始节点携带一个 `config.timeRelative` 描述符,运行时按计划扫描该对象,并为**每条匹配记录各启动一次**流程:
103+
104+
```ts
105+
export const renewalReminder = defineFlow({
106+
name: 'contract_renewal_reminder',
107+
type: 'schedule',
108+
status: 'active',
109+
nodes: [
110+
{
111+
id: 'start',
112+
type: 'start',
113+
config: {
114+
timeRelative: {
115+
object: 'contracts',
116+
dateField: 'end_date',
117+
offsetDays: [60, 30, 7], // T-minus thresholds
118+
filter: { status: 'active' }, // AND-ed with the date window
119+
},
120+
// Optional sweep cadence — defaults to daily at 08:00 UTC.
121+
schedule: { type: 'cron', expression: '0 8 * * *' },
122+
},
123+
},
124+
{ id: 'notify_owner', type: 'notify', label: 'Notify Owner' },
125+
{ id: 'end', type: 'end' },
126+
],
127+
edges: [
128+
{ id: 'e1', source: 'start', target: 'notify_owner' },
129+
{ id: 'e2', source: 'notify_owner', target: 'end' },
130+
],
131+
});
132+
```
133+
134+
|| 声明什么 |
135+
|---|---|
136+
| `object` | 扫描其记录的对象(机器名) |
137+
| `dateField` | 相对今天求值的 `date` / `datetime` 字段(按天粒度) |
138+
| `offsetDays` | **偏移模式** —— 当 `dateField` 恰好等于今天 + 所列各偏移时触发(`[60, 30, 7]`;负数 = 过去,如 `[-1]` 表示次日) |
139+
| `withinDays` | **区间模式** —— `dateField` 落在距今天 N 天内的每一天都触发:正数 = 即将到来("即将过期"),负数 = 有界的逾期回看,`0` = 今天到期 |
140+
| `filter` | 可选的 ObjectQL where 映射,与计算出的日期窗口做 AND(如 `{ status: 'active' }`|
141+
| `maxRecords` | 每次扫描启动流程的记录数上限(默认 1000;触发收拢时扫描会记日志) |
142+
143+
`offsetDays``withinDays` 必须**恰好设置一个**。另外两种常见形态:
144+
145+
```ts
146+
// "Expiring soon" — fires every day a document is within 30 days of expiry.
147+
timeRelative: { object: 'hr_document', dateField: 'expires_on', withinDays: 30 }
148+
149+
// Overdue sweep — fires for POs up to 14 days past due (bounded lookback).
150+
timeRelative: { object: 'purchase_order', dateField: 'due_date',
151+
withinDays: -14, filter: { status: 'open' } }
152+
```
153+
154+
每次启动都会把匹配记录放到自动化上下文中,因此开始节点的 `condition``{record.<field>}` 插值与记录变更流程完全一致。发现查询以 system 身份运行,并做按记录的故障隔离。`os validate` 新增就绪性检查 —— 当 `timeRelative.object` 指向 stack 未定义的对象、或自动触发的流程停留在 `draft` 状态时都会警告。
155+
156+
> **真正的"当天"检查现在没问题了(#3183)。**16.0 中 `record.due_date == today()` 能匹配了 —— 引擎会对时间类 `==` / `!=` 比较做强制转换,让日期字段与 `today()` 正确比较。真正的"今天到期"条件用它;而任何"某日期前/后 N 天"形态的需求,用 `timeRelative`
157+
99158
## Manual:Action 与审批
100159

101160
```ts

content/docs/build/automation/workflows.mdx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,15 +137,19 @@ If you're coming from a platform with Workflow Rules, here's the mapping:
137137
| Old concept | Current equivalent |
138138
|---|---|
139139
| Workflow Rule | Flow |
140-
| Time trigger | Scheduled flow |
140+
| Time trigger ("N days before/after a date") | Time-relative trigger — `config.timeRelative` on a flow's start node (16.0, see [Flows](/docs/build/automation/flows)) |
141+
| Time trigger (fixed clock) | Scheduled flow |
141142
| Field update action | `update_record` node |
142143
| Email alert | `notify` node |
143144
| HTTP call | `http` node |
144145
| Approval Process | Flow with one or more `approval` nodes |
145146

146147
The test: if the old rule was "when X happens, do Y", model it as a small
147-
flow. If it was "this record must move through controlled states", model the
148-
lifecycle as a state machine and use flows for the side effects.
148+
flow. If it was "N days before/after a date field, do Y", declare a
149+
time-relative trigger — never a date-equality condition on a record-change
150+
flow, which only evaluates when the record happens to change. If it was
151+
"this record must move through controlled states", model the lifecycle as a
152+
state machine and use flows for the side effects.
149153

150154
## Where to go next
151155

0 commit comments

Comments
 (0)