Steward
All docs

Docs How it works

Integrations and trust

Integrations are @steward's hands: GitHub, Railway, email, social posts, Stripe and Cloudflare DNS. Each one works for one business at a time, and each one goes through the action gate and the trust ladder.

Open Integrations and trust in the sidebar. Only a business's owners see its page.

Turning one on

An integration is On for a business when all three are true:

  1. Its env vars are set on the server. The page shows each one as Set or Missing, by name. It never shows the value.
  2. Its settings are filled in on the page (a repository, Railway ids, a from address...).
  3. An owner ticked Switched on for that business.

If any of these is missing, @steward's tool for it answers with what's missing and does nothing.

Secrets stay in env vars. The page only stores settings that aren't secret.

Most secrets can be set for the whole studio or for one business. The business's own env var wins. Its name is the base name, an underscore, and the business's slug in capitals, with dashes as underscores. For example, STRIPE_SECRET_KEY_DOG_WALKS for the business dog-walks.

The trust ladder

Every action has a level, per business:

LevelWhat happens
Shadow@steward logs what it would have done. Nothing happens.
SuggestA decision card: Approve, Change it or Decline. It runs only on Approve.
Spot checkIt acts, then posts what it did: Looks good, or Roll back (or Flag it when it can't be undone).
AutoIt acts alone and posts one line saying what it did.

Where each action starts depends on its risk:

RiskStarts atCan reach
internal (the studio's own records)autoauto
public (others can see it)suggestauto
person (contacts a real person)suggestauto
moneysuggestauto
irreversiblesuggestsuggest, always

How it moves:

Every step is in the ledger: action.shadow, action.run, action.rollback, trust.outcome and trust.level.

GitHub

Open a pull request from a branch, comment on one, and merge one.

Railway

Deploy the service, read deployment status, and roll back.

Services deployed with railway up

Some services have no GitHub repo linked in Railway. They're deployed with railway up from a checkout on someone's computer. For those, a deploy from the studio can't ship new code: Railway would only rebuild the last upload. So the deploy goes through the business's build agent (like @maya-agent) on its owner's computer instead. Nothing ships without an owner's say-so.

How the studio tells. Before a deploy, it asks Railway whether the service has a repo source (repoTriggers on the service). It remembers the answer for 10 minutes. If the service has one, or Railway doesn't answer, the deploy works as before.

What you approve. Without a repo source, the deploy card names:

For example: "deploy commit 1a2b3c4 of Pawpost to Railway (web) with railway up on @maya-agent's computer". The deploy runs only after an owner approves the card, or when the trust ladder has "deploy" at spot check or auto for that business.

When it runs:

  1. The studio checks again that main still points at that commit. If someone merged in the meantime, it stops and asks for a new deploy.
  2. It gives the build agent a deploy job. The job carries which card (or trust level) approved it, the commit, and the services.
  3. On the computer, the connector:
    • refuses the job if the commit isn't the head of main on GitHub
    • puts that exact commit in a deploy worktree of its own, ~/Projects/studio/<slug>-deploy, made with git worktree add from the business's main checkout, detached and clean (never the build agent's "steward" branch worktree)
    • lets Claude Code run only railway up --service <name> --project <id> --environment <id> --ci for each service, plus railway status and railway logs, with a fixed prompt: deploy these, report the deployment ids, nothing else
  4. When the job reports back, the studio finds the new deployments on Railway. From then on, the health watch and automatic rollback work as they do for any deploy. The ledger gets deploy.handoff, deploy.claimed, and then deploy.deployed, deploy.refused or deploy.failed. The product loop treats it like any other deploy.

Which services. One repo can hold more than one service (a business can have a server and a web app). List them under Services deployed with railway up on the Integrations page, as name=<service id>:paths, separated by ;:

server=80dcec7c-...:apps/server,packages; web=1f0e...:apps/web

What it needs:

What can't happen:

Email (Resend)

Send an email from the business's own address.

Social posts

Post to Bluesky, a Facebook Page and Instagram. X and LinkedIn can be added later as adapters behind the same tool.

Bluesky

Facebook Page

Instagram

WhatsApp

Message people from the business's WhatsApp number (Meta's Cloud API), and take their messages in as support tickets.

Stripe

Sell, refund, and keep revenue in step with real payments.

Campaign drafts with a daily budget cap, pause and resume, budget changes, and spend and clicks every hour, inside an owner-set monthly ad budget the hub enforces in code.

Cloudflare DNS

Read and change the domain's DNS records, and check that a change is live.

Adding an integration (for builders)

The frame is in src/server/executors/core.ts:

defineIntegration({ id: "x", name: "X", env: (b) => [[envName("X_TOKEN", b), "X_TOKEN"]], settings: ["x_account"], does: "Post on X." });

defineExecutor<{ slug: string; text: string }>({
  type: "x.post", integration: "x", risk: "public", label: "post on X",
  run: async (ctx, a) => ({ text: "Posted.", undo: { id: "..." } }),   // ctx: actor, business, settings
  rollback: async (ctx, a, undo) => "Deleted.",                         // optional
  ceiling: (b, a) => null,                                              // optional: cap the level for this call
});

gatedTool(toolDefinition, (args, ctx, business) => ({ type: "x.post", business: business.slug, args: { text: args.text }, summary: "post on X: ..." }));