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 active rule runs for real. A test rule is evaluated, but its action is only simulated: it opens no incident. inactive, paused and archived rules do not fire. Nistor sets external itself, on read-only copies of rules that another tool owns.
  • Selectors. node_ids and display_names are glob patterns (fnmatch); a pattern that also looks like a regular expression is tried as one. pause_node_ids patterns are globs only. {ENV} in a selector becomes the environment's name (dev, test or prod) 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_only overrides that for one rule. The custom_python executor and the platform job executor run every matching rule by default.
  • Tuning mode. shadow_mode: true keeps 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 validate needs no API. It parses the files and checks duplicates and the schema for dev, test and prod. It also flags any key the schema does not know, which the backend would silently drop, so a typo such as quiet_period_secnds fails here.
  • nistor rules sync takes --env dev|test|prod and --files or --dir. Useful options are --dry-run, --validate (server-side validation first), --require-rules-format (refuse a backend that would drop fields), --output-json and --continue-on-error. --delete-orphaned removes rules that are no longer in the files, scoped to --source-repo when you pass it. It needs NISTOR_API_URL and NISTOR_API_TOKEN, or the global options --api-url and --token placed before rules.
  • nistor rules list shows 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, loses or shares, and which rules compete. A rule that selects on tags alone 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.

Edit this page on GitLab

↑ ↓ to moveEnter to openEsc to close