Tide · automations

Automations, as data.

Tide is Nisc’s automation engine. An automation is a JSON document, called a reflex: what triggers it, and which function to run. Your app loads reflexes while it runs, so adding or changing one needs no deploy.

pnpm add @niscorp/tide @niscorp/prism @niscorp/strata zod · 0.1.7 · Apache-2.0

A reflex
{
  "id": "sales.report",
  "intent": "Email the sales report every day at 18:00.",
  "on": {
    "clock": {
      "every": "day",
      "at": "18:00",
      "tz": "Europe/Vienna"
    }
  },
  "effect": {
    "name": "mail.send",
    "input": {
      "to": "team",
      "report": "sales",
      "day": {
        "$ref": "$.occurrence.key"
      }
    }
  }
}

The time

17:30

What ran

The clock here is simulated. Tide takes the time as an argument, so this page passes half an hour in two seconds.

First steps →
Triggers

What can trigger a reflex.

A reflex has one trigger, in its on.

Effects

It runs a function in your app.

A reflex names its effect, and Tide calls the function your app registered under that name. So a reflex can only do what your app offers. The input is filled in from what triggered it: the row that was written, the webhook’s body, the day of the schedule.

tide.ts
import { createTide, createMemoryStore } from '@niscorp/tide';
import { evaluate } from '@niscorp/prism';

const tide = createTide({
  store: createMemoryStore(),
  transform: evaluate,
  effects: {
    'mail.send': { writes: [], run: (input) => mailer.send(input) },
  },
});

await tide.load(reflexes);

Give a reflex a select, and the function runs once for each row a query returns: one reminder per member, one charge per subscription. Each row is its own task, and fails or is retried alone.

Retries

A failed run is retried.

If your function throws, Tide runs it again: after 1 minute, then 2, then 4.

Example: a mail server that was down at 12:00

12:00 · attempt 1Failed: the mail server did not answer. The task is retrying, and the next attempt is at 12:01.
12:01 · attempt 2It went through. The task is done.
Idempotency keyEvery attempt gets the same key, ctx.taskKey, so the API you call can drop a duplicate.
Run historyEvery run and task is stored with its state and its error. A task that failed all its retries can be retried by hand.
Workflows

Chain reflexes for more than one step.

A reflex runs one function. For a second step, add a second reflex whose trigger is the first one finishing.

Step 1

{
  "id": "booking.confirm",
  "intent": "Confirm a booking by email.",
  "on": {
    "fact": {
      "entity": "bookings",
      "op": "insert"
    }
  },
  "effect": {
    "name": "mail.send",
    "input": {
      "to": {
        "$ref": "$.row.member"
      }
    }
  }
}

Step 2

{
  "id": "booking.board",
  "intent": "Then update the board.",
  "on": {
    "fact": {
      "run": "booking.confirm"
    }
  },
  "effect": {
    "name": "board.update"
  }
}

Run on this page: a booking was inserted, and Tide called mail.send, then board.update.

With AI

Safe for a model to write.

Like every document in Nisc, a reflex can be written by a model. Tide ships the agent for it: it drafts a reflex from a sentence, and checks the draft before your app gets it.

What was asked
Email the team the sales report every day at 6 pm.

What Tide’s agent did

Draft 1 · sent back to the modelThe effect "db.query" is not offered. Name one of: mail.send.
Draft 2 · acceptedIt fits the schema and names a function this page offers.
The draft your app gets
{
  "id": "sales.report",
  "intent": "Email the sales report every day at 18:00.",
  "effect": {
    "name": "mail.send",
    "input": {
      "to": "team",
      "report": "sales"
    }
  },
  "on": {
    "clock": {
      "every": "day",
      "at": "18:00",
      "tz": "Europe/Vienna"
    }
  }
}
Only the functions you offerA draft that names another function, or input that does not fit its schema, is sent back to the model.
Permissions come from CharterA draft cannot say who a reflex runs as. Set as when you save it, and resolve it to a Charter principal in actor: your functions then get that principal, and its permissions.
Nothing runs until you save itA draft is data. Your app shows it to the person who asked, and preview shows what it would do.

No model is called on this page. Both replies are written by hand, and Tide’s agent checked them.

In a Nisc app

On the server, Moss runs it.

State in PostgresMoss ships a Postgres store for Tide. Schedules, runs and tasks survive a restart.
A schedulerMoss’s driver wakes Tide when the next reflex is due, and at once when something triggers one.
Writes as triggersWith facts configured, each row written through Vex is passed to Tide, so a write can trigger a reflex.

Moss, the server your app runs on →

Asked of Tide

What an engineer asks.

Is a failed run retried?YesThree times with backoff by default. policy.retry changes it for one reflex.
Does a webhook delivered twice run twice?NoGive the signal a dedupeKey, and the second delivery is dropped.
Is there a dry run?Yespreview runs the selection and fills in each input. Your function is not called.
Can I run a reflex by hand?Yesfire triggers it now, even when it is disabled.
Does 09:00 stay 09:00 across daylight saving?YesA schedule is a local time in a named time zone.
Can I test a schedule without waiting?YesTide takes the time as an argument, advance({ now }), so a test sets the clock.
Does it poll the database?NoWrites are passed to it with ingest. In a Moss app Vex does that.
Can one reflex have several steps?NoIt runs one function. Chain reflexes for a workflow.
Can I change a reflex without a deploy?YesCall load with the changed list while the app runs.