Skip to content

Commit 4f21ce8

Browse files
authored
docs(automation): document the time-relative trigger (#1874) (#3241)
Adds the declarative time-relative trigger to the two hand-written author-facing surfaces: skills/objectstack-automation/SKILL.md (a Time-relative triggers section, framed as the replacement for the date-equality-on-record-change anti-pattern) and content/docs/automation/flows.mdx (a Time-relative flow example). The auto-generated reference and package README landed with the feature (#3230).
1 parent 43a3efb commit 4f21ce8

2 files changed

Lines changed: 87 additions & 1 deletion

File tree

content/docs/automation/flows.mdx

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,51 @@ export const contractExpirationCheck: Flow = {
735735
};
736736
```
737737

738+
### Time-relative flow (declarative date sweep)
739+
740+
Instead of hand-writing the "find records, then act" query above — and far more
741+
robust than a `record_change` flow gated on date-equality (`end_date ==
742+
daysFromNow(60)`), which only fires if the record is edited on the exact day — a
743+
`schedule` flow whose `start` node declares a `timeRelative` descriptor is swept
744+
on a schedule (daily by default) and runs **once per matching record**:
745+
746+
```typescript
747+
export const renewalReminder: Flow = {
748+
name: 'renewal_reminder',
749+
label: 'Renewal Reminder',
750+
type: 'schedule',
751+
status: 'active',
752+
runAs: 'system', // a sweep has no trigger user — elevate explicitly
753+
nodes: [
754+
{
755+
id: 'start',
756+
type: 'start',
757+
label: 'Start',
758+
config: {
759+
timeRelative: {
760+
object: 'contract',
761+
dateField: 'end_date',
762+
offsetDays: [60, 30, 7], // — or — withinDays: 30 (negative = overdue lookback)
763+
filter: { status: 'active' }, // optional, ANDed with the date window
764+
},
765+
// schedule: { type: 'cron', expression: '0 8 * * *' } // optional; defaults to daily 08:00 UTC
766+
},
767+
},
768+
{ id: 'notify_owner', type: 'notify', label: 'Notify Owner' },
769+
{ id: 'end', type: 'end', label: 'End' },
770+
],
771+
edges: [
772+
{ id: 'e1', source: 'start', target: 'notify_owner' },
773+
{ id: 'e2', source: 'notify_owner', target: 'end' },
774+
],
775+
};
776+
```
777+
778+
Exactly one of `offsetDays` (discrete T-minus days) or `withinDays` (a range) is
779+
required. Requires the `triggers` **and** `job` capabilities. The record is on
780+
the flow context (`record.*`), so the start `condition` and `{record.*}`
781+
interpolation work as in a record-change flow.
782+
738783
### Update-triggered flow
739784

740785
Trigger on a record update and compare against the previous value:

skills/objectstack-automation/SKILL.md

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ parallel. Flows are the primary automation building block in ObjectStack.
5555
|:-----|:------------|
5656
| `autolaunched` | Runs without user interaction — triggered by events, APIs, or other flows |
5757
| `screen` | Interactive — presents UI screens to the user (wizards, forms) |
58-
| `schedule` | Runs on a cron schedule (daily cleanup, weekly reports) |
58+
| `schedule` | Runs on a cron schedule (daily cleanup, weekly reports) — or a **per-record date sweep** via `config.timeRelative`, see *Time-relative triggers* |
5959
| `record_change` | Fires automatically on record create/update/delete (bind via the `start` node's `triggerType`) |
6060
| `api` | Invoked explicitly via the API / `engine.execute()`, **or** bound as an inbound **webhook**: `POST /api/v1/automation/hooks/:flowName/:hookId` (see *Inbound webhook triggers* below) |
6161

@@ -509,6 +509,47 @@ read at runtime, not Zod-validated):
509509
> value after. (Salesforce-flavor `OLD` / `NEW` were removed in M9.5 and now
510510
> evaluate to `null`.) See [objectstack-formula](../objectstack-formula/SKILL.md).
511511
512+
### Time-relative triggers — scheduled per-record date sweep
513+
514+
**Don't** express "act N days before/after a date" (renewal reminders, "expiring
515+
soon", overdue sweeps) as a `record_change` flow gated on date-equality
516+
(`end_date == daysFromNow(60)`) — that predicate is only evaluated when the
517+
record *happens to change*, so unattended it almost never fires. Use a
518+
**declarative time-relative trigger**: a `schedule`-type flow whose `start` node
519+
carries a **`timeRelative`** descriptor is swept on a schedule (daily by default)
520+
and launched **once per record** whose date field falls in the window. The record
521+
is on the context, so the start `condition` and `{record.*}` interpolation work
522+
exactly as for a record-change flow — and because the window is evaluated every
523+
day, a threshold is never missed.
524+
525+
```typescript
526+
{
527+
name: 'renewal_alert',
528+
type: 'schedule',
529+
runAs: 'system', // a sweep has no trigger user — elevate explicitly
530+
nodes: [{
531+
id: 'start', type: 'start',
532+
config: {
533+
timeRelative: {
534+
object: 'contracts',
535+
dateField: 'end_date',
536+
offsetDays: [60, 30, 7], // fire exactly at T-60 / T-30 / T-7
537+
// — or — withinDays: 30 // "expiring within 30 days" (negative = overdue lookback)
538+
filter: { status: 'active' }, // optional, ANDed with the date window
539+
// maxRecords: 1000 // optional per-sweep cap (default 1000)
540+
},
541+
// schedule: cron`0 8 * * *` // optional sweep cadence; omit for daily 08:00 UTC
542+
},
543+
}, /* …downstream nodes, connected via `edges` */],
544+
}
545+
```
546+
547+
Exactly one of `offsetDays` (discrete T-minus days) or `withinDays` (a range;
548+
negative = overdue) is required. Ships in `@objectstack/trigger-schedule`
549+
needs `requires: ['automation', 'triggers']` **plus `'job'`** (the sweep cadence
550+
runs on the job service). See the
551+
[Time Relative Trigger reference](/docs/references/automation/time-relative-trigger).
552+
512553
---
513554

514555
## Best Practices

0 commit comments

Comments
 (0)