Trigger routines from your app
Create a least-privilege API key, start runs from your own code, and poll or subscribe for the result.
The dashboard is one client of the API, not a privileged one. Anything you can do to a routine, you can do from your own code with a project-scoped key.
Create a key
Dashboard → API keys → New key. The plaintext value is shown once:
wor_<prefix>_<secret>Only a SHA-256 hash is stored. Closing the dialog removes the plaintext from the page. Keys belong to exactly one project — a key issued in one project cannot read or trigger anything in another.
Choose the least privilege the integration needs:
| Scope | Grants |
|---|---|
models:read | List the model directory. |
routines:read | List and read routines. |
routines:write | Create, update, archive, activate, and pause. |
runs:read | List runs and read run detail. |
runs:write | Start a manual run. |
connections:read | List connections and tool policies. |
connections:write | Create connections, discover tools, set policies. |
New keys default to models:read, routines:read, runs:read, and
runs:write — enough to trigger and observe, not to reconfigure.
Some operations are session-only
Creating organizations and projects, and managing API keys themselves, require
a signed-in browser session. An API key is already bound to one project, so it
cannot create the scope above itself. Those calls return 403 session_required.
Start a run
Find the routine
curl https://api.workonrepeat.com/v1/routines \
-H "Authorization: Bearer $WORK_ON_REPEAT_API_KEY"Trigger it
curl -X POST \
https://api.workonrepeat.com/v1/routines/$ROUTINE_ID/run \
-H "Authorization: Bearer $WORK_ON_REPEAT_API_KEY"The response is 202 Accepted with the queued run. The run executes on a
worker; the request does not wait for it.
{
"data": {
"id": "1f7b0e6a-...",
"routineId": "9c2e...",
"state": "queued",
"trigger": "manual",
"projectId": "5a41..."
},
"requestId": "01J..."
}Collect the result
Prefer a webhook — it arrives once, signed, and without polling. When you must poll, read the run detail and stop on a terminal state:
const TERMINAL = new Set(["succeeded", "failed", "cancelled", "timed_out"]);
export async function waitForRun(runId: string, apiKey: string) {
// Runs are bounded by their routine's timeout, so a generous ceiling here
// still terminates.
for (let attempt = 0; attempt < 120; attempt += 1) {
const response = await fetch(
`https://api.workonrepeat.com/v1/runs/${runId}`,
{ headers: { authorization: `Bearer ${apiKey}` } },
);
if (!response.ok) throw new Error(`run lookup failed: ${response.status}`);
const { data } = await response.json();
if (TERMINAL.has(data.state)) return data;
await new Promise((resolve) => setTimeout(resolve, 10_000));
}
throw new Error("run did not reach a terminal state");
}Rate limits
Authenticated /v1 traffic is limited per credential in a fixed 60-second
window. Every response carries X-RateLimit-Limit and
X-RateLimit-Remaining; a 429 also carries Retry-After and the error code
rate_limit_exceeded. Request bodies are capped at 1 MB.
Back off on Retry-After rather than retrying immediately — the window is per
credential, so a tight loop only delays your own work.
Selecting a project
An API key is bound to one project, so nothing further is needed. A browser
session can choose its active project with the X-Project-Id header, which
takes precedence over the wor_project cookie. Either way the API verifies
membership of the organization that owns the project.