Rules and scheduling
Rules that react to events or run on a schedule, the actions they trigger, and the alerts they send.
A rule tells Nistor to act: for these assets, when something happens or at a set time, and if some conditions hold, run an action. Rules keep pipelines running without a person starting each job. Most runs start with a rule, and so do most incidents (see Incidents).
Scheduling is a capability named scheduler. An administrator can disable it with
PATCH /api/v1/capabilities/scheduler and {"enabled": false}. While it is disabled, the backend refuses the rule,
calendar, on-hold, maintenance-window, backfill and chain routes. Disabling takes effect within about 30 seconds; a
capability that was disabled when the backend started comes back only after a restart. Runs, incidents, alerts and node
impact analysis stay, because Nistor also records runs of work it did not schedule, such as a Dataform run started from
outside.
Anatomy of a rule#
A rule has four blocks, read in this order:
| Block | Answers | Main fields |
|---|---|---|
applies_to |
Which assets? | node_type, node_ids, display_names, tags, exclude_node_ids, pause_node_ids |
when |
What fires it? | events, schedule, wait_for_all_ancestors |
if |
Under which conditions? | quiet_period_seconds, max_runtime_seconds, run_limit, cost_limit, max_estimated_cost_usd, hold_while_upstream_stale, touched_partition_within, refresh_sla, trigger_liveness |
then |
What does it do? | target and do (one action, or a chain) |
Two optional blocks cover lateness: failsafe runs the rule if its trigger never came, and freshness_expectation tells
a person when the target is late. Beyond the blocks, a rule has a name, a description, tags, a priority from 1 to
100 (1 by default) and a status.
- Status. An
activerule runs for real. Atestrule is evaluated, but its action is only simulated: it opens no incident.inactive,pausedandarchivedrules do not fire. Nistor setsexternalitself, on read-only copies of rules that another tool owns. - Selectors.
node_idsanddisplay_namesare glob patterns (fnmatch); a pattern that also looks like a regular expression is tried as one.pause_node_idspatterns are globs only.{ENV}in a selector becomes the environment's name (dev,testorprod) when the rule is evaluated. - Priority. When several rules match one node, most executors run only the highest-priority rule per executor type.
then.target.highest_priority_onlyoverrides that for one rule. Thecustom_pythonexecutor and the platform job executor run every matching rule by default. - Tuning mode.
shadow_mode: truekeeps a rule running and recording its runs, but stops its runs from opening incidents. The console calls it tuning mode.
Example#
This rule rebuilds a daily sales mart once the tables it reads have settled, retries a failed run, and flags the mart if it
is not fresh by 08:00 on weekdays. Its target is a node of the fictional acme estate in services/backend/nistor_backend/demo/data.json.
- name: Refresh the daily sales mart after the warehouse loads
code_id: marts/mart-sales-daily # the rule's stable identity when it is kept as a file
description: Rebuilds mart_sales_daily once its source tables have settled.
priority: 5
status: active
status_by_env: # file-only: the status per environment
dev: paused
tags:
- marts
- sales
applies_to:
node_type: DataformAction
node_ids:
- DataformAction_acme-mart-{ENV}.marts.mart_sales_daily
when:
events:
- origin: ancestors # events on the nodes this one reads from
ancestor_depth: direct_materialized
types: # matched exactly; list every write type you care about
- BIGQUERY_INSERT
- BIGQUERY_MERGE
wait_for_all_ancestors: true # fire once every source has produced a new event
wait_for_all_ancestors_timeout_seconds: 5400
wait_for_all_ancestors_timeout_alert: true
if:
quiet_period_seconds: 600 # wait for 10 quiet minutes before running
max_runtime_seconds: 7200
run_limit:
max: 3
period: day
then:
target:
same_as_applies_to: true
scope: node
do:
kind: single
action:
type: dataform_run
parameters:
release: projects/acme-mart-{ENV}/locations/europe-west1/repositories/acme-marts/releaseConfigs/daily
retry:
mode: exponential
max_retries: 3
failsafe: # acts on machines: run the rule if no trigger came
enabled: true
deadline_cron: "30 7 * * *"
time_zone: Europe/London
freshness_expectation: # acts on people: open an incident if still late
days: weekdays
before: "08:00"
time_zone: Europe/London
incident_severity: high
The acme names are fictional; use your own project and release configuration. The example passes
nistor rules validate, which checks it against the rule schema for dev, test and prod.
What fires a rule#
Events#
An event listener sits under when.events. Its fields:
| Field | Meaning |
|---|---|
origin |
Whose events: self, ancestors or descendants of the matched node. |
types |
The event types, matched exactly. A rule that listens for BIGQUERY_MERGE does not fire on BIGQUERY_INSERT. |
ancestor_depth |
With origin: ancestors, which ancestors count. direct_materialized takes the nearest materialised tables upstream, reaching through processing nodes such as Dataform actions. all takes every ancestor. |
include_node_ids, exclude_node_ids, include_node_types, exclude_node_types |
Narrow which nodes' events count. include_node_types: [DataformAction] listens to the action that builds the input. |
enrich |
Opt-in extra detail on matching events. job_stats reads the BigQuery job's metadata at no slot cost. storage_delta runs one INFORMATION_SCHEMA query, which uses slots. Both are off by default. |
poll |
For sources that send no audit-log events, such as a table in another organisation: read the source's last-modified time every interval_seconds (900 by default, from 60 to 86400) and raise the same event when it advances. It needs include_node_ids. |
statuses |
For completion listeners: which outcomes of an upstream action fire the rule (success by default, or failed, cancelled, any). |
Event types include the BigQuery write types (BIGQUERY_INSERT, BIGQUERY_UPDATE, BIGQUERY_DELETE, BIGQUERY_MERGE,
BIGQUERY_TRUNCATE, BIGQUERY_CREATE, BIGQUERY_DROP, BIGQUERY_WRITE and BIGQUERY_COPY), the completion of a
Nistor-run action (ACTION_COMPLETED, or its alias DATAFORM_RUN_COMPLETED), the action lifecycle (ACTION_STARTED,
ACTION_SUCCEEDED, ACTION_FAILED, ACTION_CANCELLED, ACTION_TO_RETRY, ACTION_RETRIED_SUCCEEDED and
ACTION_RETRIED_FAILED), DQ_FAILURE and COMPILATION_FAILURE.
Audit-log events from your monitored projects arrive on Pub/Sub at the input-logs role. It matches them to nodes in the
graph and forwards an event to the scheduler only when a rule listens for it. The scheduler evaluates the rule, applies
its if block and dispatches the action.
wait_for_all_ancestors holds the rule until every direct ancestor has produced a new event since the last run.
wait_for_all_ancestors_timeout_seconds makes it fire anyway after that long, with whatever is ready, and
wait_for_all_ancestors_timeout_alert opens an incident naming the ancestors that were still missing.
Cron#
when.schedule runs a rule on time:
| Field | Meaning |
|---|---|
cron |
One cron expression or a list. |
time_zone |
An IANA name. |
wait_for_new_data |
Fire only if new events arrived since the last run. |
business_calendar or business_calendar_id |
A calendar, by name or id, that skips non-working days. Calendars are managed under /api/v1/business_calendars. |
skip_holidays, month_boundary, market_open_offset_minutes |
With a calendar: skip holidays (the default when a calendar is set), snap to the first_business_day or last_business_day of the month, or shift the fire time by up to 720 minutes either way. |
A rule can have events and a schedule at once.
How a firing runs#
A firing that has to wait, for a quiet period, a lock held by another run or a cron time, is stored as an on-hold action with a due time. A timer calls the scheduler role once a minute, and the scheduler claims the due actions and dispatches them. In production that timer is Cloud Scheduler, and the reference Terraform creates the jobs. The Docker Compose stack in the Quickstart has no such timer: its seeded cron rules are armed, but nothing fires them. You can still run a rule by hand there.
Conditions#
if field |
Effect |
|---|---|
quiet_period_seconds |
Wait until the source has been quiet this long, so a load that writes many times fires once. With wait_for_all_ancestors, quiet_period_max_extension_seconds bounds how far new writes can push it back. |
max_runtime_seconds |
Stop the action after this long. |
run_limit |
At most max firings per hour, day, week or month. Hitting it opens an incident. |
cost_limit |
At most max_cost_usd per period. Hitting it opens an incident. |
max_estimated_cost_usd |
A per-run gate on the dry-run cost estimate. Only Dataform runs have an estimate. |
hold_while_upstream_stale |
Opt-in: hold the firing while a direct upstream asset has an open freshness breach, checking every five minutes. Only a monitored asset can hold it (see Data Trust), and a breach opened more than two hours ago no longer does. |
touched_partition_within |
Fire only when a partition that the event touched falls in a window such as the previous ISO week. It needs enrich.storage_delta on the listener. |
refresh_sla |
Opt-in: open a delivery_slo incident when a target is not refreshed within within_seconds of the first qualifying upstream write. It must exceed the quiet period, and it is evaluated only where RULE_REFRESH_SLA_ENABLED is on (off by default). |
trigger_liveness |
Tune or disable the check that a rule's triggers keep writing. The check runs only where RULE_TRIGGER_LIVENESS_ENABLED is on (off by default). |
Actions#
then.target says what to act on. By default (same_as_applies_to: true) it is the matched nodes. Otherwise you select
other nodes with node_type, node_ids, display_names or tags. scope is node, children, descendants,
ancestors or all, relative to the matched node. group: true makes one invocation for all targets.
exclusive: true stops other rules of the same executor from cascading onto the target nodes.
then.do is either one action (kind: single) or a chain (kind: chain) of steps. A step has a key, a type,
optional parameters and node_ids, the steps it runs after, and on_failure (abort, the default, or continue).
A failed action is retried according to parameters.retry:
| Field | Default | Meaning |
|---|---|---|
mode |
fixed |
fixed waits initial_backoff_s each time. exponential multiplies the wait by backoff_factor. |
initial_backoff_s |
60 | The first wait, in seconds. |
backoff_factor |
2.0 | The multiplier. It must be above 1 in exponential mode. |
max_backoff_s |
900 | The cap on one wait. |
max_retries |
0 | The number of retries. |
max_duration_s |
none | A wall-clock budget from the first attempt. When set, it replaces max_retries. |
jitter |
true |
Spread each wait by up to 20% either way. |
A rule's value overrides the executor's, and the defaults apply when neither sets a field. A value that does not
validate is rejected when you save the rule. retry_number: 3 is the older spelling of max_retries: 3.
Executors in this repository#
An executor is a package that runs one kind of action. The scheduler finds installed executors through the
nistor.executors entry-point group; GET /api/v1/rules_scheduling/executor-types lists them.
Action type |
From | Does |
|---|---|---|
dataform_run |
Google Cloud | Starts a Dataform workflow invocation. parameters.release is a release configuration path ending in /releaseConfigs/<name>; exclude_descendents_from_type, exclude_descendents_node and maximum_depth narrow the run. |
datafusion_run |
Google Cloud | Starts a Data Fusion pipeline. |
bigquery_copy |
Google Cloud | Copies BigQuery tables from one environment to another. |
bigquery_export_gcs |
Google Cloud | Exports a BigQuery table to Cloud Storage as CSV or JSON. |
microstrategy_refresh |
MicroStrategy | Refreshes a cube, runs a report or runs an incremental refresh report. |
microstrategy_event_trigger |
MicroStrategy | Triggers a MicroStrategy event. |
custom_python |
The custom Python package | Runs Python you author in a resource-limited subprocess with an import allowlist. The allowlist is a guardrail, not a security boundary: run only code you trust. |
dummy_run |
The dummy package | Simulates a success, a failure or a delay, with no external call. The local stack's seeded rules use it. |
Nistor also ships an executor that runs its own maintenance jobs (ingests, polls, clean-up sweeps) as rules. Only holders
of config:manage see those rules or can write one. Enterprise modules add executors of their own, such as
data_quality_check from the Data quality module. See Writing a tool to add one.
Running an action by hand, and saving a rule that runs it, needs the executor's permission where it has one:
actions:run_dataform, actions:run_microstrategy (both MicroStrategy actions), actions:run_bigquery_copy or
actions:run_bigquery_export_gcs. The data-engineer role template holds the Dataform and BigQuery ones;
actions:run_microstrategy is in no template but tenant-admin.
When a rule does not fire#
The two lateness blocks answer different questions, and you can use both.
| Block | Acts on | When its deadline passes without a qualifying run |
|---|---|---|
failsafe |
Machines | Nistor runs the rule's action itself. It raises no alert. Set exactly one of deadline_cron and deadline_after_seconds; the second also needs window: rolling. |
freshness_expectation |
People | Nistor opens a delivery_slo incident, with the severity you set (high by default), and resolves it once the target is observed fresh. It does not run anything. |
Rules as code#
Keep rules in a repository and let CI send them to Nistor. A rules file is YAML or JSON. It holds a list of rules (YAML
also accepts one rule). Every rule needs a code_id, and code_id and name must be unique across all files.
nistor rules validate --dir rules/
nistor rules sync --env prod --dir rules/ --dry-run
nistor rules sync --env prod --dir rules/ --delete-orphaned --source-repo your-group/your-rules
nistor rules validateneeds no API. It parses the files and checks duplicates and the schema fordev,testandprod. It also flags any key the schema does not know, which the backend would silently drop, so a typo such asquiet_period_secndsfails here.nistor rules synctakes--env dev|test|prodand--filesor--dir. Useful options are--dry-run,--validate(server-side validation first),--require-rules-format(refuse a backend that would drop fields),--output-jsonand--continue-on-error.--delete-orphanedremoves rules that are no longer in the files, scoped to--source-repowhen you pass it. It needsNISTOR_API_URLandNISTOR_API_TOKEN, or the global options--api-urland--tokenplaced beforerules.nistor rules listshows the rules stored in the API.
See CLI for installing the command.
A synced rule is locked: its source is code, and the API refuses to edit or delete it (HTTP 403, "code-managed and
locked"). Change the file instead. An emergency unlock exists: POST /api/v1/rules_scheduling/{id}/unlock. Every write
to a rule through the API, unlock included, needs actions:trigger, which the data-engineer role template includes.
Creating a rule, or an edit that can make it run, also needs the executor's permission above.
status_by_env lets one file set a different status per environment; a sync resolves it for the --env you pass. If you
delete an optional block such as if from a rule in a file, the next sync clears it in Nistor: the sync sends an
explicit empty value for every optional block that the file omits.
Rule Lab#
Rule Lab (/scheduler/rule-lab in the console) shows what a draft rule would do before you save it. Paste a rule as
JSON, or load an existing one. Nothing is stored until you save. The page shows:
- Validation: schema errors, and whether the rule matches one that is already stored, in which case saving updates it.
- Blast radius: the nodes the rule would match, and for each the outcome of the priority contest with the rules that
already hold it:
uncontested,wins,losesorshares, and which rules compete. A rule that selects ontagsalone never joins the contest, and the page says so. This is not the impact analysis of a node, described in Lineage and impact. - Replay: when the rule would have fired over the last 7, 14, 30, 60 or 90 days, for up to three nodes, next to what ran. The page picks the busiest nodes the rule would win first.
- Cost and run-rate: the measured spend, over the same window, of the nodes the rule would win. It is spend already incurred, not a forecast, and it needs the FinOps module. Without it, the nodes are listed unpriced.
An array of rules opens a batch table. Validation, blast radius, replay and saving need actions:trigger. Save appears
only when the rule is valid.
Alerts#
Alerts tell people. A channel is a destination. A policy says which events go to which channels.
| Channel type | Delivers to | Needs |
|---|---|---|
gchat |
A Google Chat space, by incoming webhook. | The webhook URL, which is a secret. |
gchat_space, gchat_dm |
A space that the Nistor Chat app belongs to, or one person's direct messages. | The Google Chat API and a registered Chat app that Nistor posts as. |
slack |
A Slack incoming webhook. | The webhook URL. |
teams |
A Microsoft Teams webhook. | The webhook URL. |
email |
The recipients you list, and the owners of the affected nodes with recipients_from: node_owners. |
The SMTP settings ALERT_EMAIL_SMTP_HOST, _PORT, _USER, _PASSWORD and _USE_TLS, and ALERT_EMAIL_FROM_ADDRESS. Node owners come from the Catalog module. |
in_app |
The console's notifications of the Nistor users you list, or of the node owners. | Nothing more. |
Cards sent to gchat_space and gchat_dm channels carry Acknowledge, Snooze and Resolve buttons only when
CHAT_SURFACE_GCHAT_AUDIENCE is set. The route that receives those clicks belongs to the AI module.
A policy has these fields:
| Field | Meaning |
|---|---|
event_types |
The events it handles, for example action_failure, action_retried_failure, incident_created, incident_resolved, long_running_action, incident_age_exceeded, freshness_sla_breach, predicted_freshness_breach and contract_breach. |
scope_type, scope_id |
global, compiler, rule or node, and the thing it names. |
min_severity, conditions |
A severity floor, and filters on environment, executor_type, node_types and incident_type. |
channel_ids |
One or more channels. Each receives the same alert. |
cooldown_seconds |
Suppress a repeat of the same alert for this long (300 by default). |
incident_dedup_seconds |
While an incident stays open, suppress re-alerts for this long (an hour when unset; 0 disables it). |
delivery_mode |
immediate, batched (with batch_window_seconds, 60 to 86400) or digest (with digest_cron: daily_9am, daily_18pm or weekly_monday_9am). |
minor_delivery |
Where the medium and low alerts of an immediate policy go: batched or one of the digest slots. ALERT_MINOR_DELIVERY_DEFAULT sets the platform default (batched). |
alert_on_recovery |
Also alert on action_success and incident_resolved (false by default, to keep flapping jobs quiet). |
quiet_hours |
Suppress non-critical alerts between two hours of the day: start_hour, end_hour and tz_offset (hours from UTC). |
Times in cards and emails are shown in ALERT_DISPLAY_TIMEZONE (Europe/Brussels by default); an unknown name falls
back to UTC.
Keep channels and policies as code the same way as rules: nistor alerts sync --env prod --dir alerts/ reads
alert_channels.json, alert_policies.json and email_settings.json. Entries carry a code_id, and a policy names its
channels with channel_code_ids. A ${VARIABLE} anywhere in a file is replaced by that environment variable of the sync
job, so a webhook URL stays out of Git. {ENV} becomes the environment name.
What needs what#
| Feature | Needs |
|---|---|
| Rules, events, cron, conditions, chains, rules as code, Rule Lab and the executors above | Open source, with the scheduler capability. |
| Alert channels and policies | Open source. |
refresh_sla, trigger_liveness |
Their flags, RULE_REFRESH_SLA_ENABLED and RULE_TRIGGER_LIVENESS_ENABLED, both off by default. |
Data-quality checks as rules (data_quality_check) |
The Data quality module. |
| Spend and event counts in Rule Lab | The FinOps module. Without it, Rule Lab lists the nodes unpriced. |
| Owner-based alert recipients | The Catalog module. |
| Buttons on Google Chat alert cards | A registered Chat app, CHAT_SURFACE_GCHAT_AUDIENCE, and the AI module. |
The modules are part of Nistor Enterprise. The flags are environment variables of the backend; see Configuration.