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}` }),
}),
);
});
import json
from workers import fetch
from shiftlabs import workflow
@workflow
async def refund_check(ctx):
# ctx.secrets.STRIPE_KEY is a stand-in: the gateway swaps in the key.
async def fetch_charge():
r = await fetch(f"https://api.stripe.com/v1/charges/{ctx.payload['id']}",
headers={"authorization": f"Bearer {ctx.secrets.STRIPE_KEY}"})
return await r.json()
charge = await ctx.step.run("fetch charge", fetch_charge)
await ctx.step.sleep("cool-off", "10 minutes")
async def notify():
await fetch("https://hooks.slack.com/services/T0/B0/x", method="POST",
body=json.dumps({"text": f"Refund check for {charge['id']}"}))
await ctx.step.run("notify", notify)
- 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
- queued21:30:14.227
- started989 ms21:30:15.216
- fetch charge212 ms21:30:15.301
- cool-off10 min21:30:15.514
- notify95 ms21:40:15.902
- completed21:40:16.010
- 1,000 runs a dayfree, with 10 workflows
- 390–1,000 msfrom queued to started
- 22–150 mscold load of a version
- US regionsruns execute there only
- Start free
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
| Status | Run | Rev | Started |
|---|---|---|---|
| Sleeping | 316d72be | A | 21:41:10 |
| Completed | 7d10ce5b | A | 21:30:15 |
| Completed | a91c03d4 | A | 21:12:06 |
Revisions · refund-check
| Rev | Hash | Deployed |
|---|---|---|
| A current | 1d9a281e | 2026-10-20 |
316d72be
Sleeping · until 21:51:10- Version
1d9a281e· rev A- Trigger
kQ7v2xNs9fA1bE3cRtWy0g· signed: stripe- Dispatches
- 1
- queued21:41:09.698
- started704 ms21:41:10.402 · cold load 64 ms
- fetch charge209 ms21:41:10.468
- cool-off10 min21:41:10.679
| Request | Status | Time |
|---|---|---|
| GET api.stripe.comvia connection stripe | 200 | 176 ms |
- Deploy B while
316d72besleeps: 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.
Run 316d72be sleeps on revision A until 21:51:10.
What every run keeps
| A | 1d9a281e |
Pinned to its revision
A run finishes on the code it started on.
Secrets scrubbed
Readable secret values never reach the run logs.
| Request | Status | Time |
|---|---|---|
| GET api.stripe.comvia connection stripe · 21:30:15 | 200 | 182 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.
Outbound requests
2Every request on record
Host, method, status, connection and outcome, kept with the run.
Signed webhooks
Stripe, GitHub or shiftlabs signatures, within 5 minutes. Replays are dropped.
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.
Trigger
POST to
hooks.shiftlabs.org/t/…from anywhere. Signatures are checked and replays dropped; the hook answers 202 with the run id.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.
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
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 workspace
X-Shiftlabs-TenantandX-Abuse-Contactride on every outbound requestevery request