ShiftLabs

Runbooks and AI agents in code, with a person approving the risky steps

rev A · 1d9a281e
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}` }),
    }),
  );
});
  • 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

  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

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.

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.

  1. 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.

  2. Deploy it and connect a trigger

    npx shiftlabs deploy ships 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.

  3. 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

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

Last 24 h: 3 of 1,000 runs · 5 of 3,000 outbound requests · 1 of 10 concurrent

Runs, newest first
StatusRunRevStarted
Sleeping316d72beA21:41:10
Completed7d10ce5bA21:30:15
Completeda91c03d4dashboardA21:12:06
  • Deploy B while 316d72be sleeps: 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.

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.

workflows/restart.tsTypeScript
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.

shiftlabs.config.tsinvoice-assistant · v3
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 · v3
  1. You 21:44:02

    Refund invoice in_1QxYz8Lk. The customer was charged twice.

  2. Invoice assistant 21:44:05

    Called stripe-invoice · done

    Called stripe-refund · held for approval

  3. Invoice assistant 21:44:07

    Invoice in_1QxYz8Lk was paid: $240.00. The refund is waiting for an approver.

  4. 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"}

  5. Invoice assistant 21:47:34

    Refunded $240.00 on invoice in_1QxYz8Lk.

  1. Why did 316d72be finish on revision A?

  2. ShiftLabs helper

    Called get_run · done

    It started on A at 21:41:10. A run finishes on the code it started on. New runs use B.

  3. I want an agent that triages GitHub issues.

  4. 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.

TemplateTools
GitHub Actions runbook2 workflows1 waits for approval
Invoice assistant2 workflows1 waits for approval
GitHub issue triage3 workflows2 wait for approval
Support triage1 workflow1 waits for approval
Lead qualifier2 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

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 are redacted from run logs and from tool results an agent sees.

RequestStatusTime
GET api.stripe.comvia connection stripe · 21:30:15200182 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.

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. 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.

The templates
TemplateWhat it doesWaits for approval
GitHub Actions runbookgithub-actions-runbookRuns a runbook in your own GitHub Actions, waits for its result, and lists recent runsStarting the runbook
GitHub issue triagegithub-triageReads an issue, suggests labels and a reply, and posts themThe comment and the labels
Support triagesupport-triageSorts a support message by kind and urgency, drafts a reply, and posts the triage to your team's Slack channelThe Slack post
Lead qualifierlead-qualifierLooks up a lead in HubSpot, rates it hot, warm or cold with reasons, and records the rating as a noteThe HubSpot note
Invoice assistantinvoice-assistantAnswers questions about Stripe invoices and refunds themThe 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.

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 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.deploy or run.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

Limits on the Free and Pro plans
LimitFreePro
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 limit1,000a day250,000a month, included
Runs at onceOver it, the webhook returns 42910100
StepsPast the cap, Free returns 429. Pro bills past the included steps, up to your spending limit5,000a day1,000,000a month, included
CPU timePast the cap, Free returns 429. Pro bills past the included time, up to your spending limit300 sa day50,000 sa month, included
Outbound requestsAnd 30 a minute per host on both. Over that, the gateway returns 4293,000a day1,000,000a month, included
Workflows1050
Active triggers20100
Secrets and connections25and 10250and 100
AgentsThe built-in helper doesn't count325
Model allowanceFor agents and the helper. Small models on Free, larger models on Pro50,000tokens a day, $1 a month5,000,000tokens a day, $10 a month included
Longest a run can lastAt most 50 engine dispatches per run168 h168 h
CPU per runThe run's next dispatch fails once its code has used it10 s60 s
CPU per invocationWith 50 subrequests per invocation2,000 ms2,000 ms
Runner tokensAnd the longest a command on a runner can run230 minutes2012 hours
Turns per message, at most6 by default. A conversation's budget goes up to $52020

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-Tenant and X-Abuse-Contact headersevery request

Is it right for you?

Who ShiftLabs fits today, and who it doesn't fit yet.

Questions

Common questions before you sign up. More on pricing

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.