Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions content/docs/automation/flows.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,51 @@ export const contractExpirationCheck: Flow = {
};
```

### Time-relative flow (declarative date sweep)

Instead of hand-writing the "find records, then act" query above — and far more
robust than a `record_change` flow gated on date-equality (`end_date ==
daysFromNow(60)`), which only fires if the record is edited on the exact day — a
`schedule` flow whose `start` node declares a `timeRelative` descriptor is swept
on a schedule (daily by default) and runs **once per matching record**:

```typescript
export const renewalReminder: Flow = {
name: 'renewal_reminder',
label: 'Renewal Reminder',
type: 'schedule',
status: 'active',
runAs: 'system', // a sweep has no trigger user — elevate explicitly
nodes: [
{
id: 'start',
type: 'start',
label: 'Start',
config: {
timeRelative: {
object: 'contract',
dateField: 'end_date',
offsetDays: [60, 30, 7], // — or — withinDays: 30 (negative = overdue lookback)
filter: { status: 'active' }, // optional, ANDed with the date window
},
// schedule: { type: 'cron', expression: '0 8 * * *' } // optional; defaults to daily 08:00 UTC
},
},
{ id: 'notify_owner', type: 'notify', label: 'Notify Owner' },
{ id: 'end', type: 'end', label: 'End' },
],
edges: [
{ id: 'e1', source: 'start', target: 'notify_owner' },
{ id: 'e2', source: 'notify_owner', target: 'end' },
],
};
```

Exactly one of `offsetDays` (discrete T-minus days) or `withinDays` (a range) is
required. Requires the `triggers` **and** `job` capabilities. The record is on
the flow context (`record.*`), so the start `condition` and `{record.*}`
interpolation work as in a record-change flow.

### Update-triggered flow

Trigger on a record update and compare against the previous value:
Expand Down
43 changes: 42 additions & 1 deletion skills/objectstack-automation/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ parallel. Flows are the primary automation building block in ObjectStack.
|:-----|:------------|
| `autolaunched` | Runs without user interaction — triggered by events, APIs, or other flows |
| `screen` | Interactive — presents UI screens to the user (wizards, forms) |
| `schedule` | Runs on a cron schedule (daily cleanup, weekly reports) |
| `schedule` | Runs on a cron schedule (daily cleanup, weekly reports) — or a **per-record date sweep** via `config.timeRelative`, see *Time-relative triggers* |
| `record_change` | Fires automatically on record create/update/delete (bind via the `start` node's `triggerType`) |
| `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) |

Expand Down Expand Up @@ -509,6 +509,47 @@ read at runtime, not Zod-validated):
> value after. (Salesforce-flavor `OLD` / `NEW` were removed in M9.5 and now
> evaluate to `null`.) See [objectstack-formula](../objectstack-formula/SKILL.md).

### Time-relative triggers — scheduled per-record date sweep

**Don't** express "act N days before/after a date" (renewal reminders, "expiring
soon", overdue sweeps) as a `record_change` flow gated on date-equality
(`end_date == daysFromNow(60)`) — that predicate is only evaluated when the
record *happens to change*, so unattended it almost never fires. Use a
**declarative time-relative trigger**: a `schedule`-type flow whose `start` node
carries a **`timeRelative`** descriptor is swept on a schedule (daily by default)
and launched **once per record** whose date field falls in the window. The record
is on the context, so the start `condition` and `{record.*}` interpolation work
exactly as for a record-change flow — and because the window is evaluated every
day, a threshold is never missed.

```typescript
{
name: 'renewal_alert',
type: 'schedule',
runAs: 'system', // a sweep has no trigger user — elevate explicitly
nodes: [{
id: 'start', type: 'start',
config: {
timeRelative: {
object: 'contracts',
dateField: 'end_date',
offsetDays: [60, 30, 7], // fire exactly at T-60 / T-30 / T-7
// — or — withinDays: 30 // "expiring within 30 days" (negative = overdue lookback)
filter: { status: 'active' }, // optional, ANDed with the date window
// maxRecords: 1000 // optional per-sweep cap (default 1000)
},
// schedule: cron`0 8 * * *` // optional sweep cadence; omit for daily 08:00 UTC
},
}, /* …downstream nodes, connected via `edges` */],
}
```

Exactly one of `offsetDays` (discrete T-minus days) or `withinDays` (a range;
negative = overdue) is required. Ships in `@objectstack/trigger-schedule` —
needs `requires: ['automation', 'triggers']` **plus `'job'`** (the sweep cadence
runs on the job service). See the
[Time Relative Trigger reference](/docs/references/automation/time-relative-trigger).

---

## Best Practices
Expand Down