Every agent listed in scripts/deploy/agents.json
can be deployed through this path — today that is every folder in the repo with a
persona.ts, and a test keeps the two in step. This guide takes you from a fork
to a running agent in your Agent Workforce workspace, without an interactive
login and without a terminal if you don't want one.
You need two secrets. The rest is a form in the Actions tab.
- 1. What you need first
- 2. Get the two secrets
- 3. Put them in GitHub
- 4. Deploy from the Actions tab
- 5. Give the agent its inputs
- 6. Deploy from your own shell instead
- 7. When it fails
- 8. Adding your own agent
An Agent Workforce workspace. If you don't have one, create it at https://agentrelay.com/cloud. A workspace is where deployed agents live, where their integrations (Slack, Linear, GitHub, Gmail…) are connected, and what the two secrets below identify.
The integrations your chosen agent needs, already connected in that
workspace. CI deploys with --no-connect, which reuses connections that
already exist and refuses to start an OAuth flow — a browser consent screen is
not something a CI job can complete. Section 7 covers what
that failure looks like and how to fix it once.
Node 22+ and this repo, if you want the local path in section 6. The Actions path needs neither.
| Name | What it is | Where it goes |
|---|---|---|
WORKFORCE_WORKSPACE_ID |
Which workspace to deploy into (a UUID; not sensitive) | Environment variable |
WORKFORCE_WORKSPACE_TOKEN |
The token that authorises deploys into it (grants write; sensitive) | Environment secret |
These are the deploy CLI's documented headless-auth pair: when both are set, the CLI skips the browser session entirely. That is the whole reason this works in CI.
Both come from one login on your own machine:
npm install # the agentworkforce CLI is a devDependency of this repo
npx agentworkforce login # opens your browser, then asks which workspacelogin stores your cloud session in ~/.agentworkforce/relay/cloud-auth.json
and the selected workspace in ~/.agentworkforce/relay/workspaces.json (both
created 0600, readable only by you).
Two look-alikes that do not work.
workspaces.jsonholds a relaycast workspace key (rk_live_…) and relaycast ids look likerw_…. Neither is what the deploy API wants: therk_key is rejected with 401 and therw_id with 403 Forbidden. Newer CLIs refuse anrk_token up front.
WORKFORCE_WORKSPACE_TOKEN is the cloud access token from your login session:
# prints a live credential, so don't do this on a shared screen
jq -r '.accessToken' ~/.agentworkforce/relay/cloud-auth.json
# when it stops working:
jq -r '.accessTokenExpiresAt' ~/.agentworkforce/relay/cloud-auth.jsonThis token expires. When a deploy starts failing with 401, run
npx agentworkforce login again and update the secret.
WORKFORCE_WORKSPACE_ID is the workspace's cloud workspace id (a UUID).
CLIs that include AgentWorkforce/workforce#342
print it after login (cloud workspace id: …). With an older CLI, ask the cloud
to resolve your active workspace:
AUTH=~/.agentworkforce/relay/cloud-auth.json
curl -fsS \
-H "Authorization: Bearer $(jq -r '.accessToken' "$AUTH")" \
"$(jq -r '.apiUrl' "$AUTH")/api/v1/workspaces/$(jq -r '.workspaces[.active].key' ~/.agentworkforce/relay/workspaces.json)/resolve" \
| jq -r '.cloudWorkspaceId'If you have several workspaces, replace .workspaces[.active] with the one you
want, e.g. .workspaces["other-workspace"]. If that prints null, your relay workspace isn't linked to a cloud workspace
yet — contact support rather than substituting another id.
If
logincan't list your workspaces (some accounts get a 403 on the workspace list), pass the workspace explicitly and it will skip the listing step:npx agentworkforce login --workspace <id-or-slug>.
Treat the token like a password. It authorises deploys into your workspace; anyone holding it can deploy agents there. Nothing in this repo ever prints it — the deploy script reports the names of missing secrets and the names of the inputs it resolved, never their values.
In your fork, go to Settings → Environments → New environment, name it exactly:
workforce
Then add:
WORKFORCE_WORKSPACE_IDunder Environment variables (the workspace UUID is not sensitive; leaving it in variables keeps it visible in the run summary for debugging). If you already have it under Environment secrets that keeps working — the workflow reads whichever is set, preferring the variable. Nothing needs migrating.WORKFORCE_WORKSPACE_TOKENunder Environment secrets (the token is sensitive and must never appear in logs).
The name workforce matters: the deploy job declares environment: workforce,
and that declaration is what makes both the variable and the secret resolve.
Get the name wrong and the job runs with empty strings rather than failing to
start — which is why the workflow checks for empty values in a dedicated step
and tells you exactly this.
An environment also gets you the optional extras: required reviewers before a deploy runs, and a branch rule limiting which refs can deploy.
Repository secrets work too. A job that references an environment can still read repository secrets, so you may put the two secrets at the repository level and leave the workflow alone; an environment secret of the same name simply takes precedence. Keep the
environment: workforceline either way — deleting it also gives up the required-reviewer and branch protections above.
Actions → Deploy agent → Run workflow.
| Field | What to put |
|---|---|
| agent | The agent's name, exactly as listed in scripts/deploy/agents.json — e.g. hn-monitor |
| agent-inputs | Optional KEY=VALUE lines — see section 5 |
| dry-run | Tick it for a rehearsal: compiles, resolves inputs, prints the deploy, and stops |
The run installs dependencies, runs the test suite and the typechecker, compiles the persona, and deploys. Deploys are idempotent — dispatching the same agent again updates the existing one rather than failing on the name it already took, so re-running after a config change is the normal way to change an agent.
Start with dry-run ticked. It needs no secrets and no integrations, it prints the exact command the real run will make, and because it compiles the persona first it will also tell you if a required input is unset — before you have wired up any secrets at all.
This repo is public, so anyone can open a pull request against it from a fork.
The deploy workflow therefore runs only on workflow_dispatch, which only
someone with write access to the repo can start.
Do not add pull_request_target — and do not add any trigger that runs a fork's
code with these secrets in scope. A fork PR can change the deploy script, or any
file the job executes, and a job holding WORKFORCE_WORKSPACE_TOKEN when it does
so hands over your workspace. There is a comment saying this at the top of the
workflow, and a test in tests/deploy-agents.test.mjs that fails if the trigger
ever appears.
If you fork this and want deploy-on-merge, add push: on your own default
branch. That runs your code, not a stranger's.
Agents are configured with inputs — a Slack channel to post in, a threshold
to alert on, a list of topics to watch. Every persona declares its own, so the
authoritative list for any agent is the inputs block in its persona.ts. Each
one gives you a description, the environment variable name it reads, whether it
has a default, and whether it is optional.
For example, vendor-monitor/persona.ts
declares VENDORS (defaulted) and SLACK_CHANNEL (required, no default).
Rules the deploy script applies:
- An input with a default and no value set uses its default.
- An input marked
optionaland left unset is simply not passed. This is usually a feature switch: noSLACK_CHANNEL, no Slack delivery. - Any other unset input fails the deploy before anything is deployed, naming the input, its description, and the environment variable to set.
- Empty is unset. A secret that exists but holds
""counts as missing, so a mis-pasted secret fails loudly instead of deploying a misconfigured agent.
A few agents watch a specific account: daytona-monitor (DAYTONA_ORG_ID),
gcp-watcher (GCP_PROJECT_ID), neon-monitor (NEON_ORG_ID), and
pr-shepherd (ORG). Their personas ship a default for these — and that
default is this repo's owner's org, project, or account, not yours.
Inheriting it would point your agent at someone else's infrastructure: at best
an authorisation error, at worst a monitor reporting on a tenant that isn't
yours. So scripts/deploy/agents.json lists those input names under
requireExplicit, and the deploy refuses to fall back to the persona default
for them:
✗ gcp-watcher: 1 required input(s) unresolved:
GCP_PROJECT_ID — GCP project id to monitor.
set env GCP_PROJECT_ID=<value>, or pass --input GCP_PROJECT_ID=<value>
(this one identifies YOUR account or project. Its persona default points at
someone else's, so this deploy will not inherit it.)
Set your own value and it deploys. If you add an agent whose persona defaults to
an org, project, or account id, add that input name to requireExplicit too — a
test fails if you don't.
Every input the deploy resolves is printed with where it came from
(--input, AGENT_INPUTS, env NAME, or persona default), so you can see at
a glance in a dry run whether anything is being inherited that shouldn't be.
They stack in this order, each overriding the one before:
- Repository variable
AGENT_INPUTS— Settings → Secrets and variables → Actions → Variables. Your standing defaults for every deploy. - Secret
AGENT_INPUTS— same page, Secrets tab. For values that must stay masked in the run log, like an API token. - The
agent-inputsbox on the dispatch form — a one-off override for this run.
All three take the same format, one KEY=VALUE per line (# comments and blank
lines are ignored):
SLACK_CHANNEL=C0123ABCD
TOPICS=agent runtimes, evals
# lines below override lines above
Anything you type into the dispatch form is visible in the run log. A
channel id is fine there; a token is not — put that in the AGENT_INPUTS
secret, where GitHub masks it.
Same script, same flags, same result:
npm install
# See what is deployable.
node scripts/deploy/deploy-agents.mjs --list
# Rehearse: compiles, resolves inputs, prints the deploy. No secrets needed,
# nothing is deployed. (Compiling writes gitignored persona.json/agent-card.json.)
node scripts/deploy/deploy-agents.mjs --agent hn-monitor --dry-run
# Deploy for real.
# The deploy script requires both variables, even after `npx agentworkforce login`.
# Use the values from section 2:
export WORKFORCE_WORKSPACE_ID="<cloud-workspace-uuid>"
export WORKFORCE_WORKSPACE_TOKEN="$(jq -r '.accessToken' ~/.agentworkforce/relay/cloud-auth.json)"
export SLACK_CHANNEL=C0123ABCD # or: --input SLACK_CHANNEL=C0123ABCD
node scripts/deploy/deploy-agents.mjs --agent hn-monitorInputs resolve from the environment under the name each persona declares, so
export SLACK_CHANNEL=… is all a local deploy usually needs. --input K=V
overrides that for a single run, and --all deploys every agent in the registry.
There is also npm run deploy -- --agent hn-monitor if you prefer.
missing headless auth: WORKFORCE_WORKSPACE_TOKEN
The secret is empty or absent. In Actions this nearly always means the
environment is named something other than workforce, or the secret was added
as a repository secret while the job asks for an environment one. Locally it
means the variable isn't exported.
hn-monitor: 1 required input(s) unresolved
An input the persona requires has no value anywhere. The message names the input
and the environment variable to set; see section 5.
Nothing was deployed.
integrations.slack: not connected, and --no-prompt was passed. Connect it before deploying or run without --no-prompt.
This is the one that catches people out. The agent needs an integration your
workspace hasn't connected, and CI cannot complete an OAuth consent screen.
Connect it once from your own machine by deploying that agent interactively — without the headless flags, so the CLI can walk you through the browser flow:
npx agentworkforce persona compile ./hn-monitor/persona.ts
npx agentworkforce deploy ./hn-monitor/persona.json --mode cloudThen check what your workspace has, and re-dispatch the workflow:
npx agentworkforce integrations --allpersona requires a subscription provider connection, but --no-prompt was passed.
Same shape of problem, for the LLM credential rather than an integration: that
persona sets useSubscription: true and your workspace has no provider
connected. Fix it the same way — one interactive deploy, then re-dispatch.
skipping slack channels picker for input SLACK_CHANNEL because --no-prompt is set
A warning, not an error. Interactively the CLI would have shown you a channel
picker; headlessly you supply the value yourself. If the deploy then fails on an
unresolved input, that's the one to set.
-
Create
<your-agent>/persona.tsand<your-agent>/agent.ts, following any existing folder. -
Add it to
scripts/deploy/agents.json:{ "name": "your-agent", "persona": "your-agent/persona.ts" } -
npm test— a test fails if an agent on disk is missing from that registry, so you'll be told if you skip step 2.
Declare inputs in your persona.ts, not in the registry. The deploy script
reads them from the compiled persona, so a new input needs no change here — and
no channel id, org id, or token ever has to be committed to a public repo.