shiftlabs

Automations in TypeScript and Python, run at the edge.

rev A · 1d9a281e
import { workflow } from "shiftlabs";

export default workflow(async ({ payload, step, secrets }) => {
  // secrets.STRIPE_KEY is a stand-in: the gateway swaps in the key.
  const charge = await step.run("fetch charge", () =>
    fetch(`https://api.stripe.com/v1/charges/${payload.id}`, {
      headers: { authorization: `Bearer ${secrets.STRIPE_KEY}` },
    }).then((r) => r.json()),
  );
  await step.sleep("cool-off", "10 minutes");
  await step.run("notify", () =>
    fetch("https://hooks.slack.com/services/T0/B0/x", {
      method: "POST",
      body: JSON.stringify({ text: `Refund check for ${charge.id}` }),
    }),
  );
});
  • Started by any webhook
  • Pinned to its code
  • Gateway adds the keys
  • Every request recorded
  • Durable steps and sleeps

7d10ce5b refund-check

Completed 10 min 2 s

rev A · 2 dispatches

  1. queued21:30:14.227
  2. started989 ms21:30:15.216
  3. fetch charge212 ms21:30:15.301
  4. cool-off10 min21:30:15.514
  5. notify95 ms21:40:15.902
  6. completed21:40:16.010

Steer a run

Deploy a new revision while a run sleeps, send an unsigned webhook, unlink a connection, or point the fetch where the gateway refuses to go. The record changes by the same rules as in production.

This sample workspace plays in your browser and sends nothing. Start here: deploy revision B while 316d72be sleeps, send a webhook, then skip 10 minutes. How runs stay on their version

Rows of telephone operators in headsets at a long switchboard marked with destinations: London, Paris, Bermuda, ship-to-shore; a supervisor stands behind them.
Women working at a Bell System international telephone switchboard
Runs ridgeline-ops · refund-check 21:41:30 UTC
Runs, newest first
StatusRunRevStarted
Sleeping316d72beA21:41:10
Completed7d10ce5bA21:30:15
Completeda91c03d4A21:12:06

Revisions · refund-check

RevHashDeployed
A current1d9a281e2026-10-20

316d72be

Sleeping · until 21:51:10
Version
1d9a281e · rev A
Trigger
kQ7v2xNs9fA1bE3cRtWy0g · signed: stripe
Dispatches
1
  1. queued21:41:09.698
  2. started704 ms21:41:10.402 · cold load 64 ms
  3. fetch charge209 ms21:41:10.468
  4. cool-off10 min21:41:10.679
Outbound requests
RequestStatusTime
GET api.stripe.comvia connection stripe200176 ms
developer+
  • Deploy B while 316d72be sleeps: it wakes on revision A; new webhooks start on B.
  • Unsigned delivery: hooks answers 401 invalid signature and no run starts.
  • stripe connection unlinked: the request leaves without a key and Stripe answers 401.
  • fetch to 203.0.113.7: the gateway answers 403; IP addresses are refused.
  • Viewer: can read every run but cannot re-run one.

What every run keeps

Call records clipped in sheaves along the top of a switchboard at Fort Richardson, Alaska, in 1950, above an operator's cords.
Fort Richardson switchboard, 1950
A1d9a281e

Pinned to its revision

A run finishes on the code it started on.

21:30:15.490logusing token [redacted]

Secrets scrubbed

Readable secret values never reach the run logs.

RequestStatusTime
GET api.stripe.comvia connection stripe · 21:30:15200182 ms

Keys added at the gateway

Your code passes a stand-in for the key. The gateway swaps in the real one for api.stripe.com only, over https only.

From “Operator Toll Dialing: Teamwork”, a film for telephone operators, about 1949

Outbound requests

2

Every request on record

Host, method, status, connection and outcome, kept with the run.

401 invalid signature

Signed webhooks

Stripe, GitHub or shiftlabs signatures, within 5 minutes. Replays are dropped.

Sleeping · until 21:40:15 UTC

Durable sleeps

Sleep for ten minutes or up to a week. The run wakes on the same version.

GET 203.0.113.7 blocked

requests to IP addresses are not allowed; use a hostname

Refused at the gateway

IP addresses, ports other than 80 and 443, and shiftlabs.org itself. Raw TCP has no way out.

From webhook
to run

Any sender can start a run: Stripe, GitHub, a cron job, your own code. Each delivery is queued, pinned to the version current when it arrived, and run at the edge.

  1. Trigger

    POST to hooks.shiftlabs.org/t/… from anywhere. Signatures are checked and replays dropped; the hook answers 202 with the run id.

  2. Queue

    Each delivery waits its turn on the version that was current when it arrived. Queued to started: 390–1,000 ms in our runs.

  3. Run

    The pinned code loads in 22–150 ms and runs its steps in US regions. Results are stored; sleeps resume on the same code.

Built for the people who get paged

Read the security model
Women's Army Corps operators in headsets work a long switchboard side by side.
WAC operators on the afternoon shift, Potsdam, 1945

Sign-in

  • MFAEvery sign-in needs a second factorAuthenticator-app codes (TOTP), plus bot and fraud detectionevery member
  • 30 dSessions end after 30 days, or 7 idleA __Host- cookie holding a random token; we keep only its SHA-256every session
  • 5 minRole changes reach live sessionsEach session re-reads its role; removing a member ends their sessions at onceevery session
  • 15 minFails closedIf the identity provider is unreachable, a session lasts 15 more minutes, then endsevery session

Roles

  • viewerReads everything a run leaves behindRuns, logs, timelines, outbound requests, usage; trigger ids shown maskedviewer and up
  • developerDeploys, triggers and re-runsWorkflows, triggers, vars and a workflow's connectionsdeveloper and up
  • adminHolds the keysSecrets, connections, API keys, members and the audit logadmin only

API keys

  • 256 bitsOne workspace and one role per keyA key's role can never exceed its creator'sadmins create
  • 90 dEvery key expiresAnywhere from 1 to 365 days; 90 by defaultper key
  • OnceShown at creation, then never againWe store its SHA-256, never the keyper key
  • 20Active keys per workspaceRevoke one without touching anyone else'sper workspace
  • 10 / minFailed attempts per IPAfter that the API answers 429per IP

Limits

  • 1,000Runs per 24 hours, 10 at oncePast either, the webhook answers 429free tier
  • 3,000Outbound requests per 24 hoursAnd 30 a minute per host; the gateway answers 429free tier
  • 10WorkflowsWith 20 active triggers, 25 secrets and 10 connectionsfree tier
  • 168 hLongest a run can liveAt most 50 engine dispatches per runevery run
  • 2,000 msCPU per invocationWith 50 subrequests per invocationevery run

Abuse

  • DeployEvery deploy is scannedWebAssembly, crypto-mining code and raw sockets are blockedevery deploy
  • HourlyUsage is checked every hourTraffic piled on one host, failing targets and blocked requests go to an operatorevery workspace
  • OffThe kill switchA suspended workspace has its active runs stopped and its webhooks refusedoperators
  • t_…Every request names its workspaceX-Shiftlabs-Tenant and X-Abuse-Contact ride on every outbound requestevery request