|
| 1 | +--- |
| 2 | +title: Time Relative Trigger |
| 3 | +description: Time Relative Trigger protocol schemas |
| 4 | +--- |
| 5 | + |
| 6 | +{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} |
| 7 | + |
| 8 | +Time-Relative Trigger Protocol |
| 9 | + |
| 10 | +A **declarative** trigger for time-relative business rules — "act on records |
| 11 | + |
| 12 | +whose date field is coming up (or overdue) relative to today" — without the |
| 13 | + |
| 14 | +author hand-writing a cron job + range query, and without the fragile |
| 15 | + |
| 16 | +date-equality-on-record-change anti-pattern (#1874). |
| 17 | + |
| 18 | +## The anti-pattern it replaces |
| 19 | + |
| 20 | +Authors used to express "alert 60 days before `end_date`" as a `record_change` |
| 21 | + |
| 22 | +flow gated on `end_date == daysFromNow(60)`. That predicate is only evaluated |
| 23 | + |
| 24 | +when the record *happens to change*, so it fires only if the record is edited |
| 25 | + |
| 26 | +on exactly that day — i.e. almost never, unattended. The robust alternative |
| 27 | + |
| 28 | +was a hand-written `schedule` flow that queries a date range every day, which |
| 29 | + |
| 30 | +every author re-implemented (contracts `renewal_alert`, hr |
| 31 | + |
| 32 | +`document_expiring_soon`, procurement `po_overdue`, …). |
| 33 | + |
| 34 | +## What this declares instead |
| 35 | + |
| 36 | +A `time_relative` trigger sweeps an object on a schedule (daily by default) |
| 37 | + |
| 38 | +and launches the flow **once per matching record**, with that record in the |
| 39 | + |
| 40 | +automation context (so `\{record.<field>\}` interpolation and the start-node |
| 41 | + |
| 42 | +`condition` gate work exactly as they do for record-change flows). The |
| 43 | + |
| 44 | +descriptor is carried on the flow's start node as `config.timeRelative`. |
| 45 | + |
| 46 | +@example T-minus renewal reminders (fires on the day a contract is 60/30/7 days out) |
| 47 | + |
| 48 | +```ts |
| 49 | + |
| 50 | +// flow start node |
| 51 | + |
| 52 | +config: \{ |
| 53 | + |
| 54 | +timeRelative: \{ |
| 55 | + |
| 56 | +object: 'contracts', |
| 57 | + |
| 58 | +dateField: 'end_date', |
| 59 | + |
| 60 | +offsetDays: [60, 30, 7], |
| 61 | + |
| 62 | +filter: \{ status: 'active' \}, |
| 63 | + |
| 64 | +\}, |
| 65 | + |
| 66 | +// optional sweep cadence — defaults to daily at 08:00 UTC |
| 67 | + |
| 68 | +schedule: \{ type: 'cron', expression: '0 8 * * *' \}, |
| 69 | + |
| 70 | +\} |
| 71 | + |
| 72 | +``` |
| 73 | + |
| 74 | +@example "Expiring soon" range (fires every day a document is within 30 days of expiry) |
| 75 | + |
| 76 | +```ts |
| 77 | + |
| 78 | +config: \{ |
| 79 | + |
| 80 | +timeRelative: \{ object: 'hr_document', dateField: 'expires_on', withinDays: 30 \}, |
| 81 | + |
| 82 | +\} |
| 83 | + |
| 84 | +``` |
| 85 | + |
| 86 | +@example Overdue sweep (fires for POs up to 14 days past due) |
| 87 | + |
| 88 | +```ts |
| 89 | + |
| 90 | +config: \{ |
| 91 | + |
| 92 | +timeRelative: \{ object: 'purchase_order', dateField: 'due_date', withinDays: -14, filter: \{ status: 'open' \} \}, |
| 93 | + |
| 94 | +\} |
| 95 | + |
| 96 | +``` |
| 97 | + |
| 98 | +<Callout type="info"> |
| 99 | +**Source:** `packages/spec/src/automation/time-relative-trigger.zod.ts` |
| 100 | +</Callout> |
| 101 | + |
| 102 | +## TypeScript Usage |
| 103 | + |
| 104 | +```typescript |
| 105 | +import { TimeRelativeTrigger } from '@objectstack/spec/automation'; |
| 106 | +import type { TimeRelativeTrigger } from '@objectstack/spec/automation'; |
| 107 | + |
| 108 | +// Validate data |
| 109 | +const result = TimeRelativeTrigger.parse(data); |
| 110 | +``` |
| 111 | + |
| 112 | +--- |
| 113 | + |
| 114 | +## TimeRelativeTrigger |
| 115 | + |
| 116 | +### Properties |
| 117 | + |
| 118 | +| Property | Type | Required | Description | |
| 119 | +| :--- | :--- | :--- | :--- | |
| 120 | +| **object** | `string` | ✅ | Object (machine name) to sweep, e.g. "contracts". | |
| 121 | +| **dateField** | `string` | ✅ | Date or datetime field evaluated relative to today, e.g. "end_date". | |
| 122 | +| **withinDays** | `integer` | optional | Range mode: fire while dateField is within N days of today. Positive = upcoming, negative = overdue lookback, 0 = today. | |
| 123 | +| **offsetDays** | `integer[]` | optional | Offset mode: fire when dateField is exactly today + each offset (e.g. [60, 30, 7]). | |
| 124 | +| **filter** | `Record<string, any>` | optional | Extra ObjectQL where-map ANDed with the date window (e.g. `{ status: "active" }`). | |
| 125 | +| **maxRecords** | `integer` | optional | Max records launched per sweep (default 1000). The sweep logs when it clamps. | |
| 126 | + |
| 127 | + |
| 128 | +--- |
| 129 | + |
0 commit comments