import { workflow } from "shiftlabs";
export default workflow(async ({ payload, step }) => {
// No Authorization header: the stripe connection sets it.
const charge = await step.run("fetch charge", () =>
fetch(`https://api.stripe.com/v1/charges/${payload.id}`)
.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):
# No Authorization header: the stripe connection sets it.
async def fetch_charge():
r = await fetch(f"https://api.stripe.com/v1/charges/{ctx.payload['id']}")
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 an alert, a schedule or an agent
- Risky steps wait for a yes in Slack
- Commands run on a runner you host
- Pinned to its code
- Every request recorded
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, no card required
- $49 a monthPro, with 250,000 runs included
- Your runnercommands run inside your network
- Approvalson every plan, Free included
- Start free
When the pager goes off, where does the fix live?
At a company without a platform team, it's usually in one of three places.
- Nowhere written down
The fix is in the head of whoever handled it last time, and that person is on vacation.
- A Word doc of steps
Someone on call copies commands out of it at 2 a.m. and hopes the doc still matches the servers.
- A script only one person trusts
It runs from their laptop, with their credentials, and nobody else is sure what it touches.
- With ShiftLabs
The steps become a workflow that starts from the alert, stops for approval before anything risky, and keeps a record of every step it ran.
How it works
Write workflows in TypeScript or Python and start them from an alert, a cron schedule or an agent. Commands run inside your network through a runner you host, and the steps you mark as risky wait for a yes in Slack.
Write the runbook as a workflow
Start from a template with
npx shiftlabs init --template github-actions-runbook, or write one in TypeScript or Python. It lives in your repo like any other code.Deploy it and connect a trigger
npx shiftlabs deployships your workflows, agents and schedules together. Point an alert's webhook at the trigger URL, set a cron schedule, or hand the workflow to an agent as a tool.Approve the risky steps from Slack
The run stops at each approval step and posts Approve and Deny buttons in Slack. Once someone approves, the run carries on. Its timeline shows every step and outbound request.
Steer a run
Deploy a new revision while a run sleeps, send an unsigned webhook, unlink a connection, or fetch a blocked host.
This sample workspace runs in your browser. Nothing is sent. Start here: deploy revision B while 316d72be sleeps, send a webhook, then skip 10 minutes. Close the open run to see the list. How runs stay on their version
Last 24 h: 3 of 1,000 runs · 5 of 3,000 outbound requests · 1 of 10 concurrent
| Status | Run | Rev | Started |
|---|---|---|---|
| Sleeping | 316d72be | A | 21:41:10 |
| Completed | 7d10ce5b | A | 21:30:15 |
| Completed | a91c03d4dashboard | A | 21:12:06 |
Run 316d72be
- Status
- Sleeping · until 21:51:10
- Version
1d9a281e· rev A- Dispatches
- 1
- Started by
- trigger
kQ7v2xNs9fA1bE3cRtWy0g
Timeline
- 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 |
| Rev | Hash | Date | By |
|---|---|---|---|
| A this run current | 1d9a281e | 2026-10-20 | ci-deploy |
- Deploy B while
316d72besleeps: it wakes on revision A. New webhooks start on B. - Unsigned delivery: hooks returns 401 invalid signature. No run starts.
- stripe connection unlinked: the request goes out without a key. Stripe returns 401.
- fetch to 203.0.113.7: the gateway returns 403. IP addresses are not allowed.
- Viewer: can read runs but can't re-run them.
- Re-run: a new run on the same version and payload. Started by names who asked for it.
Run 316d72be sleeps on revision A until 21:51:10.
Your runbook becomes code you can review
Each step's result is stored, failed steps retry, and a run can sleep for ten minutes or a week. A run finishes on the code it started on.
- Start runs from an alert, a schedule or an agent
Every trigger gets a webhook URL. Stripe, GitHub and ShiftLabs signatures are checked, replays are dropped, and the sender gets a
202with the run's id. Schedules use cron in your time zone. You can also start a run from the dashboard, the API or an agent's tool call. - Risky steps wait for a yes in Slack
Add
step.waitForApprovalto a workflow, or mark an agent's toolapproval: "always". The run stops until an admin or the approvers you name answer, in Slack, the dashboard or the API. Require two approvers where one isn't enough. Requests nobody answers count as denied after 72 hours. Approvals are on the Free plan. - Run commands next to your servers
A runner is a container you start in your own network, on Docker or Kubernetes.
step.execsends it a command, and it sends back the exit code and output. It makes outbound HTTPS calls only, so you open no inbound port. Commands use the credentials you give the runner. They can run for 30 minutes on Free and 12 hours on Pro. - Runbooks already in GitHub Actions
A workflow can start one in your repo, wait for its result, and check GitHub if no result arrives.
- One release
Workflows, agents and schedules ship together and roll back with one command. The GitHub Action can deploy on every push.
- Pro overage stops at a number you set
The spending limit is $100 a month by default, from $0 to $10,000, and at most $50 until your subscription's first monthly renewal. The billing contact gets an email at 80% and at 100%. At the limit, new runs return
429and runs already started finish.
import { workflow } from "shiftlabs";
export default workflow<{ deployment: string }>(async ({ payload, step }) => {
// Runs on your runner, inside your network.
await step.exec("list pods", { command: ["kubectl", "get", "pods", "-n", "apps"] });
const decision = await step.waitForApproval("restart", {
message: `Restart ${payload.deployment}?`,
approvers: ["role:admin"],
});
if (!decision.approved) return { restarted: false, note: decision.note };
await step.exec("restart", {
command: ["kubectl", "rollout", "restart", `deployment/${payload.deployment}`, "-n", "apps"],
});
return { restarted: true };
});
Agents that call your workflows
An agent has instructions, a model and tools. A tool is one of your workflows, or a function in the agent's own code.
Agents are on in every new workspace. An admin can turn them off in Settings → Agents.
tools: [
workflowTool("stripe-invoice", {
description: "Look up a Stripe invoice.",
}),
workflowTool("stripe-refund", {
description: "Refund a paid invoice.",
approval: "always",
}),
],
maxTurns: 6,
budget: { conversationUsd: 0.25 },
stripe-refundruns out in 2 d 23 h
{"invoice": "in_1QxYz8Lk", "reason": "charged twice"}
Invoice assistant · conversation 5e1c40aa, asked by you@example.com
Held for approval
Calls to a tool marked approval: "always" wait for an approver, with the exact arguments that will run. Unanswered requests count as denied after 72 hours.
Conversation 5e1c40aa
Invoice assistant · v3You 21:44:02
Refund invoice in_1QxYz8Lk. The customer was charged twice.
Invoice assistant 21:44:05
Called
stripe-invoice· doneCalled
stripe-refund· held for approvalInvoice assistant 21:44:07
Invoice in_1QxYz8Lk was paid: $240.00. The refund is waiting for an approver.
Approval update 21:47:31
an approver approved stripe-refund, and it ran with the arguments that were approved.
{"refunded": true, "refund": "re_3QxZb2Lk", "status": "succeeded", "amount": 24000, "currency": "usd"}Invoice assistant 21:47:34
Refunded $240.00 on invoice in_1QxYz8Lk.
Why did 316d72be finish on revision A?
ShiftLabs helper
Called
get_run· doneIt started on A at 21:41:10. A run finishes on the code it started on. New runs use B.
I want an agent that triages GitHub issues.
ShiftLabs helper
Start from the github-triage template:
npx shiftlabs init --template github-triage. Its labels and replies wait for an approver.
The built-in helper
It knows the platform and reads your workspace with your access. Its tools only read.
| Template | Tools |
|---|---|
| GitHub Actions runbook | 2 workflows1 waits for approval |
| Invoice assistant | 2 workflows1 waits for approval |
| GitHub issue triage | 3 workflows2 wait for approval |
| Support triage | 1 workflow1 waits for approval |
| Lead qualifier | 2 workflows1 waits for approval |
Five templates
Deploy one from the dashboard, or start a project from it with npx shiftlabs init.
$ npx shiftlabs deploy
release 14
workflow stripe-invoice: 4be0a6d2 (unchanged)
workflow stripe-refund: 8d1f22c0 (was 61c0e9b7)
agent invoice-assistant: v3 (was v2)
One release
Workflows and agents change together. Roll back with one command.
- Turns for the last message
- 1 of 6
- Model cost
- $0.0006 of $0.25
- Model allowance
- 12,480 tokens of 50,000 in 24 h
Budgets and turn limits
Each conversation has a budget and each message a turn limit. Your plan sets the model allowance.
What every run keeps
| A | 1d9a281e |
Pinned to its revision
A run finishes on the code it started on.
Secrets scrubbed
Readable secret values are redacted from run logs and from tool results an agent sees.
| Request | Status | Time |
|---|---|---|
| GET api.stripe.comvia connection stripe · 21:30:15 | 200 | 182 ms |
Keys added at the gateway
Your code sends no key. The stripe connection adds it, only for api.stripe.com and only over https.
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. Raw TCP goes only to targets an admin declares, and never to SMTP ports.
Five templates to start from
Deploy one from the dashboard, or write it into a project with npx shiftlabs init. All five run on a small model, so they work on Free.
| Template | What it does | Waits for approval |
|---|---|---|
GitHub Actions runbookgithub-actions-runbook | Runs a runbook in your own GitHub Actions, waits for its result, and lists recent runs | Starting the runbook |
GitHub issue triagegithub-triage | Reads an issue, suggests labels and a reply, and posts them | The comment and the labels |
Support triagesupport-triage | Sorts a support message by kind and urgency, drafts a reply, and posts the triage to your team's Slack channel | The Slack post |
Lead qualifierlead-qualifier | Looks up a lead in HubSpot, rates it hot, warm or cold with reasons, and records the rating as a note | The HubSpot note |
Invoice assistantinvoice-assistant | Answers questions about Stripe invoices and refunds them | The refund |
Built for the people who get paged
When a run fails or an agent's tool call waits for an approver, we email the members you pick and post to your webhook. Each post is signed, retried up to four times, and kept in a delivery log.
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 with a random token. We store only its SHA-256every session - 5 minRole changes reach live sessionsEach session rechecks its role. Removing a member ends their sessions immediatelyevery session
- 15 minFails closedIf the identity provider is unreachable, a session lasts 15 more minutes, then endsevery session
Roles
- 403Every route checks a capabilitySuch as
workflow.deployorrun.view. Each role holds the ones at or below itevery request - viewerReads runs and talks to agentsRuns, logs, timelines, outbound requests and usage. Trigger ids are maskedviewer and up
- developerDeploys, triggers and re-runsWorkflows, agents, triggers, vars and a workflow's connectionsdeveloper and up
- adminHolds the keys and answers approvalsSecrets, connections, alerts, API keys, members and the audit log. Turns agents on and offadmin only
API keys
- SomeFewer capabilities than its roleA CI key can hold only
workflow.deploy. A key never holds more than its creatoradmins create - ScopeLimited to named agents or workflowsThe key sees only those and what belongs to them. Other routes refuse itper key
- 90 dEvery key expires1 to 365 days, 90 by defaultper key
- OnceShown once, at creation256 random bits, for one workspace. We store only its SHA-256per key
- 20Active keys per workspaceRevoking one doesn't affect the othersper workspace
- 10 / minFailed attempts per IPThen the API returns 429per IP
Plans
| Limit | Free | Pro |
|---|---|---|
| PriceFree and Pro have the same features. Only the limits differ | $0no card | $49a month |
| RunsPast the cap, Free returns 429. Pro bills past the included runs, up to your spending limit | 1,000a day | 250,000a month, included |
| Runs at onceOver it, the webhook returns 429 | 10 | 100 |
| StepsPast the cap, Free returns 429. Pro bills past the included steps, up to your spending limit | 5,000a day | 1,000,000a month, included |
| CPU timePast the cap, Free returns 429. Pro bills past the included time, up to your spending limit | 300 sa day | 50,000 sa month, included |
| Outbound requestsAnd 30 a minute per host on both. Over that, the gateway returns 429 | 3,000a day | 1,000,000a month, included |
| Workflows | 10 | 50 |
| Active triggers | 20 | 100 |
| Secrets and connections | 25and 10 | 250and 100 |
| AgentsThe built-in helper doesn't count | 3 | 25 |
| Model allowanceFor agents and the helper. Small models on Free, larger models on Pro | 50,000tokens a day, $1 a month | 5,000,000tokens a day, $10 a month included |
| Longest a run can lastAt most 50 engine dispatches per run | 168 h | 168 h |
| CPU per runThe run's next dispatch fails once its code has used it | 10 s | 60 s |
| CPU per invocationWith 50 subrequests per invocation | 2,000 ms | 2,000 ms |
| Runner tokensAnd the longest a command on a runner can run | 230 minutes | 2012 hours |
| Turns per message, at most6 by default. A conversation's budget goes up to $5 | 20 | 20 |
An admin upgrades to Pro in Settings → Plan and billing. Pro overage is billed up to a spending limit you set. Need custom limits or a contract? Email sales@shiftlabs.org. See full pricing
Abuse
- DeployEvery deploy is scannedWebAssembly and crypto-mining code are blocked. Raw TCP goes only to targets an admin declares, never to SMTP portsevery deploy
- HourlyUsage is checked every hourHeavy traffic to one host, failing targets and blocked requests are flagged to an operatorevery workspace
- OffThe kill switchSuspending a workspace stops its active runs and rejects its webhooksoperators
- t_…Every request names its workspaceOutbound requests include the
X-Shiftlabs-TenantandX-Abuse-Contactheadersevery request
Is it right for you?
Who ShiftLabs fits today, and who it doesn't fit yet.
- A good fit
You run ops or SRE on a small team without a platform team, and the runbooks are a doc, a script or someone's memory.
- A good fit
You want commands to run inside your network without opening a port to the internet.
- A good fit
You're building AI agents that need to change things in GitHub, Stripe, HubSpot or your own systems, with a person signing off first.
- Not a fit yet
Your runs or data must stay in one country or region. Workspaces aren't pinned to a region yet.
- Not a fit yet
You need to host the whole platform yourself, or run it air-gapped. ShiftLabs is hosted: we run the platform for you, and the runner is the only part that runs on your side.
Questions
Common questions before you sign up. More on pricing
- Do I need a card to start?
No. Free needs an account with a second factor and nothing else. An admin upgrades to Pro in Settings → Plan and billing.
- What happens when I hit a limit?
On Free, the caps are per 24 hours, and past a cap new runs return
429. On Pro, usage past the included amounts is billed as overage up to your spending limit. At the limit, new runs return429and runs already started finish. - Can ShiftLabs reach servers inside my network?
Yes, through a runner you host on Docker or Kubernetes. It makes outbound HTTPS calls only, so you open no inbound port, and it uses the credentials you give it.
- Where do runs execute?
Workspaces aren't pinned to a region yet. Runs and stored data can be outside the US.
- Who can deploy, approve or change settings?
Every member signs in with an authenticator-app code. Roles are checked on every request: viewers read runs and talk to agents, developers deploy and re-run, and admins hold the keys and answer approvals. API keys can be limited to named workflows and expire after 90 days by default.
- Which languages can I write workflows in?
TypeScript and Python. Both have an SDK, and the Python package comes with type hints and a testing kit.
Start with one runbook
Deploy the GitHub Actions runbook template, or your own workflow, on Free. You get 1,000 runs a day, 3 agents, and approvals in Slack from the first run.
- Start free
1,000 runs a day. No card required. Create a workspace
- Compare plans
Free and Pro have the same features. Only the limits differ. See pricing
- Read the docs
Workflows, runners, approvals and agents. Open the docs