Runtime and organizations
A loop is a class you wrote, but a run is what actually happens when the platform calls run(). This page covers how runs execute, and how loops sit inside an organization.
How a run executes
Every trigger — UI, API, or schedule — produces a single run. A run is an isolated execution of one version of your loop:
- The platform picks the loop’s currently-active version. New deploys never affect runs that are already in flight.
- Your
run()is invoked in a sandboxed environment with the trigger payload available asevent.body. - The run gets a fresh
runIdthat everything (steps, logs, connector calls, output) is recorded against.
Runs don’t share state with each other. If you need state between runs, persist it outside the loop and read it back in on the next trigger.
Steps are checkpointed
step.do(name, fn) is the unit of durability. When a step succeeds, its return value is persisted against the run. If the loop is retried for any reason — transient failure, suspension, platform-side recovery — already-succeeded steps are not re-run: their stored return values are replayed.
A few things follow from that:
- Return values must be JSON-serializable. Functions, class instances,
undefinedvalues, etc. won’t survive a replay. Stick to plain objects, arrays, strings, numbers, booleans,null. - Side effects belong inside a step, not between steps. Code at the top level of
run()re-executes on every replay. Anything that hits the outside world — HTTP calls, connector calls, writes — should be inside its ownstep.doso it runs exactly once. - Step names must be unique within a run. They’re how the platform identifies a step across replays, and they’re what shows up in the run timeline.
async run(event: HealthEvent, step: HealthStep) {
// Runs on every replay — fine for pure setup
const { patientId } = event.body;
// Runs once. The return value is persisted and replayed on retry.
const profile = await step.do("Load profile", async () => {
const res = await fetch(`https://api.example.com/patients/${patientId}`);
return res.json();
});
return { profile };
}Waiting without burning compute
step.waitForScan({ timeout }) suspends the run until the scan finishes (or the timeout fires). While suspended, the loop isn’t consuming compute — it’s parked. When the external event arrives, the run resumes from that line with the result.
The same applies to scheduled triggers: a loop that waits doesn’t keep a worker pinned.
What gets recorded
Without you doing anything extra, each run captures:
- Steps — every
step.doandstep.waitForScan, in order, with status, timing, and return value. - Event log — every
console.log,console.error, etc., tagged to the run. - Input / output — the trigger payload and whatever
run()returned. - Connector trace — every connector call made from inside a step.
You don’t need to instrument anything. Logging is console.*; tracing is “use step.do.”
Loops live inside an organization
An organization is the boundary for everything you build on the platform. When you sign up, you create (or join) one. Inside it:
- Loops are owned by the organization, not by individual users. Anyone in the org can view, edit, and deploy them.
- Versions stack up per loop — each deploy adds one. Rollback picks a previous version of that same loop.
- Runs belong to the loop, and therefore to the organization. They’re not visible to other orgs.
- API tokens are org-scoped. A token can only trigger loops in the org that minted it.
- Members are people invited into the organization. Invites go out by email; once accepted, the member can act on the org’s loops.
- Connectors are configured at the organization level and made available to every loop in it.
The trigger URL itself encodes the relationship:
POST https://api.ollie.health/loop/$ORG_ID/$LOOP_IDThe org ID in the path and the bearer token in the header have to match — a token from one org can’t trigger another org’s loops, even if you have the URL.
Versions and active deploys
Each Deploy writes a new version. One version per loop is marked active; that’s the one new triggers use. Older versions stay queryable under Version history, and rolling back is a metadata flip — no code is rebuilt.
Runs that started against an older version finish on that version. The active pointer only affects new runs.
Where to go next
- Build your first Loop — if you haven’t yet, this is the five-minute path from empty workspace to a triggered run.
- The Overview page maps the same vocabulary (instructions, triggers, connectors, outputs, event log) onto a worked example.