This is the full developer documentation for Reminix
# Introduction
> Reminix makes your team's scripts, and the ones AI writes, safe for the whole team and its agents to run.
AI makes code cheap to write, but a script on one laptop helps only the person who wrote it. Publish it to Reminix, and people on your team who don’t write code run it from a form in the app. Your software runs it through the API, and AI agents use it over MCP.
Who can run it, which API keys it gets and which runs need a person’s approval are settings, not code. Every change is a new version, and every run is on record.
A few words you’ll see throughout these docs:
* A **script** is code you publish to Reminix: one function with a name, an owner and described inputs. Its code lives in **versions**, and runs use the one that’s **published**.
* A **run** is one execution: its inputs, output, log, and who started it from where.
* A **secret** is an API key your scripts use, attached by Reminix to requests to the hosts you allow, so the code never holds it.
## Where to start
[Section titled “Where to start”](#where-to-start)
* [Quickstart](/docs/quickstart/): publish a first script and run it, in about five minutes.
* [Publish a script with your agent](/docs/agents-onboarding/): let Claude Code or Codex prepare a script you have and publish it.
* [How Reminix works](/docs/how-it-works/): where code runs, how Reminix keeps secrets, and who can do what.
Everything you can do in the app, you can also do through the [API](/docs/authentication/), the [command line](/docs/cli/) and the [MCP server](/docs/mcp/).
# Use with AI agents
> Let an AI agent work in your workspace through the MCP server, the command line or the API, and teach it how with the agent skill.
AI agents can do in your workspace what you could do yourself, within the access you give them. They act as **you**, and the audit log records what they did. There are three ways to connect one, from least setup to most:
| Your agent | Use |
| --------------------------------------- | ------------------------------------------------------ |
| Claude, ChatGPT or another app with MCP | The [MCP server](/docs/mcp/). Nothing to install. |
| A coding agent with a terminal | The [command line](/docs/cli/), with `--json`. |
| Your own software | The [API](/docs/quickstart/) or the [SDK](/docs/sdk/). |
## MCP server
[Section titled “MCP server”](#mcp-server)
Add `https://mcp.reminix.com/mcp` as a remote MCP server in your agent. You sign in once in the browser and choose **one workspace** and what the agent may do there, and that’s all it can do. See [MCP server](/docs/mcp/) for the details and safeguards.
## Command line
[Section titled “Command line”](#command-line)
Coding agents such as Claude Code, Cursor and Codex work well with the [command line](/docs/cli/):
```bash
npm install -g @reminix/cli
reminix login # you approve it in the browser
reminix workspace list --json
reminix workspace webhook-endpoints list --workspace acme --json
```
With `--json`, the agent gets the API’s exact JSON, errors as JSON on stderr, and an exit code of 0 or 1. In CI, or a sandbox without a browser, set `REMINIX_TOKEN` to a [personal access token](/docs/authentication/) limited to what the agent needs.
## Teach your agent: the skill
[Section titled “Teach your agent: the skill”](#teach-your-agent-the-skill)
The agent skill is one file that teaches an agent everything on this page: how to sign in, choose the workspace, every command and operation, the error codes, and what to leave to a person. It’s at [`/docs/skill/SKILL.md`](https://reminix.com/docs/skill/SKILL.md), and it’s generated from the API, so it always matches.
To install it for every project in Claude Code:
```bash
mkdir -p ~/.claude/skills/reminix
curl -fsSL https://reminix.com/docs/skill/SKILL.md \
-o ~/.claude/skills/reminix/SKILL.md
```
Or put it in a project’s `.claude/skills/reminix/`. Other agents that support skills take the same file in their own skills folder.
## Docs for agents
[Section titled “Docs for agents”](#docs-for-agents)
The documentation is also available as plain text: [`/docs/llms.txt`](/docs/llms.txt) indexes every page, and [`/docs/llms-full.txt`](/docs/llms-full.txt) has everything in one file. The site’s [`/llms.txt`](https://reminix.com/llms.txt) points to these, the API, the MCP server and the skill.
## What agents can’t do
[Section titled “What agents can’t do”](#what-agents-cant-do)
Some operations stay with people, even when an agent has full access: anything that sends your workspace’s data somewhere new or gives out access, such as creating API keys or adding webhook endpoints. The agent asks instead, and you approve or deny (see [Approvals](/docs/approvals/)). A secret created this way never reaches the agent.
# Publish a script with your agent
> Let Claude Code or Codex prepare a script in your repository for your team, test it, and publish it only when you say so.
Your coding agent can prepare a script you already have for your team. Its inputs become a form and an agent tool, secrets stay out of the code, and the agent proves the draft works before you decide to publish.
## 1. Connect the agent
[Section titled “1. Connect the agent”](#1-connect-the-agent)
Connect it to Reminix’s [MCP server](/docs/mcp/), or let it use the [command line](/docs/cli/), and give it the skill that explains how Reminix works ([Use with AI agents](/docs/agents/)).
## 2. Paste this prompt
[Section titled “2. Paste this prompt”](#2-paste-this-prompt)
```text
Publish scripts/ to Reminix. Search the team's
scripts first in case one exists. Describe its inputs in the input
schema with clear titles, write a short README, keep every secret out of
the code (ask me to save it as a Reminix secret) and declare only the
hosts it calls. Upload it as a draft, test-run it with realistic inputs,
show me the result, and ask me before publishing.
```
## 3. What it does
[Section titled “3. What it does”](#3-what-it-does)
1. `reminix init` creates `reminix.json`, `README.md` and an entry file with `run(inputs, ctx)`.
2. It fills in the inputs, the README and the code. It ports a Python script to JavaScript, keeping its inputs and behaviour.
3. `reminix scripts publish` uploads a **draft**. Nobody else can run it yet.
4. It test-runs the draft: `reminix scripts run --version --input '{…}' --log`. Reminix marks a test run as one, doesn’t count it as the team’s use, and starts no triggers or webhooks from it.
5. It shows you the result and asks you to publish, in the app or with `reminix scripts publish --publish`. If your workspace requires a second person to publish, you get an approval link.
## Test runs
[Section titled “Test runs”](#test-runs)
The script’s owner, workspace owners and admins, and the person who uploaded a version can test-run that version:
```bash
curl -X POST https://api.reminix.com/v1/scripts/nightly-report/versions/3/runs \
-H "Authorization: Bearer $REMINIX_TOKEN" -H "X-Workspace: acme" \
-H "Content-Type: application/json" -d '{ "inputs": { "team": "sales" } }'
```
The script’s approval policy still applies to test runs.
# Approvals
> When an agent needs something it may not do alone, such as an API key or a webhook, it asks and a person decides.
Some actions are never an agent’s to take alone, because they give out access or send your data somewhere new: creating an API key, or adding, removing or re-enabling a webhook endpoint. An agent that needs one **asks**, and a person who could do it themselves decides.
## How it works
[Section titled “How it works”](#how-it-works)
1. **The agent asks,** with the action, its details and a reason, such as “I’m connecting acme-web and need an API key that can read the workspace”. Everyone who could approve it gets a notification.
2. **A person decides,** from the notification or the link the agent shows. You see exactly what will happen and why, and choose **Approve** or **Deny**.
3. **It happens as you,** with your permissions. The audit log records it under your name, along with the agent that asked.
A request expires after 24 hours, and a person can decide it only once.
## Secrets stay out of the agent’s hands
[Section titled “Secrets stay out of the agent’s hands”](#secrets-stay-out-of-the-agents-hands)
When the action creates a secret, such as an API key or a webhook signing secret, the agent never sees it:
* **Asked from the command line:** after you approve, the command line writes the secret straight into a file in your project. It’s never printed, so an agent working in the terminal can’t read it.
```bash
reminix workspace approval-requests create --action workspace.apiKey.create \
--input '{"name":"acme-web","scopes":["workspace:read"]}' \
--reason "Connect acme-web" --json
reminix workspace approval-requests redeem --wait --write-env .env
# Done: … Wrote REMINIX_API_KEY to .env (the value is not shown).
```
* **Asked by an agent over MCP:** the action runs when you approve, and **you** see the secret, once, on the approval page.
## For developers
[Section titled “For developers”](#for-developers)
`GET /v1/workspace/approval-actions` lists what an agent can ask for, each with its input schema and who approves it. `POST /v1/workspace/approval-requests` asks, and `GET /v1/workspace/approval-requests/{id}` reports the decision. Asking needs the `workspace.approvals:write` scope and a person’s credential: a personal access token, or an app a person connected, but not a workspace API key.
# Authentication
> Authenticate API requests with a workspace API key, a personal access token, or OAuth.
The public API accepts a **workspace API key** in the standard Bearer header:
```http
Authorization: Bearer YOUR_API_KEY
```
```bash
curl \
-H "Authorization: Bearer YOUR_API_KEY" \
https://api.reminix.com/v1/workspace
```
The response names the workspace the key belongs to:
```json
{ "id": "…", "name": "Example Workspace" }
```
## API keys belong to a workspace
[Section titled “API keys belong to a workspace”](#api-keys-belong-to-a-workspace)
A key acts as a workspace, not a person. It keeps working if the person who created it leaves the workspace. It never carries a user session, so nobody can use it to sign in.
## Creating a key
[Section titled “Creating a key”](#creating-a-key)
Workspace owners create keys in the app, under **Settings → API Keys**:
1. Give it a name that says where it’s used, such as `production-backend`.
2. Choose what it may do (see [Scopes](#scopes)). It’s read-only unless you choose otherwise.
3. Copy the secret. We show it once and you can’t view it again, so put it in your secret manager straight away.
The list of keys shows only what is safe to show: the name, the first few characters, the access, and when it was created and last used. If a secret is lost, create a new key and revoke the old one.
## Revoking and rotating
[Section titled “Revoking and rotating”](#revoking-and-rotating)
Owners can revoke a key at any time under **Settings → API Keys**. It stops working on the next request, and you can’t restore it.
To rotate a key without downtime:
1. Create a new key.
2. Switch your integration to it.
3. Revoke the old key.
## Scopes
[Section titled “Scopes”](#scopes)
You give every key and token **scopes** when you create it, and it can do only what they allow. A scope names a group of endpoints and whether it may read or change them: `workspace:read` reads the workspace profile, `workspace.members:read` reads members and pending invitations, and `workspace.audit:read` reads the audit trail. The [API reference](https://api.reminix.com/v1/docs) shows the scope each endpoint needs.
When you create a credential, you choose:
| Access | What it gets |
| --------------- | ------------------------------------------------------------------------------------------- |
| **Read only** | The default. Every read scope that exists at creation. Scopes added later are not included. |
| **Full access** | Everything, including scopes added to the API later. |
| **Custom** | Exactly the scopes you tick. |
Give an integration only what it needs. Calling an endpoint the credential wasn’t given returns `403 forbidden`:
```json
{
"error": {
"code": "forbidden",
"message": "This credential is not granted the workspace.audit:read scope",
"requestId": "…"
}
}
```
You can’t change a credential’s scopes after you create it. To give an integration different access, create a new credential and revoke the old one.
## Personal access tokens
[Section titled “Personal access tokens”](#personal-access-tokens)
A personal access token (`pat_…`) acts as **you**, not as a workspace. Use one for your own scripts, CI jobs and the command line. Create it in the app under **Account → API tokens**, or let the command line create one. Its `login` opens your browser, and once you approve, the command line creates the token for that computer and stores it for you.
A personal access token:
* works in the workspaces you choose when you create it, all of yours or specific ones, with the [scopes](#scopes) you give it;
* is also limited by your role in each workspace. For example, `GET /v1/workspace/audit-events` is for owners only, as the audit page is in the app. (A workspace API key is the workspace itself and has no role.)
* expires when you choose: after 30 days, 90 days, a year, or never;
* identifies you at `/v1/user/me`:
```bash
curl \
-H "Authorization: Bearer YOUR_PERSONAL_ACCESS_TOKEN" \
https://api.reminix.com/v1/user/me
```
```json
{
"id": "…",
"name": "Casey Customer",
"email": "casey@example.com",
"workspaces": [
{ "id": "…", "name": "Example", "slug": "example", "role": "owner" }
]
}
```
Everywhere else, a personal access token has to say which workspace it’s acting in, with the `X-Workspace` header set to the workspace’s slug. The call then runs with your membership in that workspace and the token’s scopes:
```bash
curl \
-H "Authorization: Bearer YOUR_PERSONAL_ACCESS_TOKEN" \
-H "X-Workspace: example" \
https://api.reminix.com/v1/workspace
```
Without `X-Workspace` the request fails with `400`. A workspace you’re not a member of, one the token wasn’t created for, or a scope the token doesn’t have, fails with `403`. Workspace API keys ignore the header, because a key already is its workspace, and they’re never accepted on `/v1/user/*`.
Like API keys, we show tokens once, and you can revoke them from the same page. They stop working at once if they expire or your account is suspended.
## Apps and AI agents (OAuth)
[Section titled “Apps and AI agents (OAuth)”](#apps-and-ai-agents-oauth)
Apps, including AI agents connecting their tools, can act for a person without ever holding a key, because the product is an OAuth 2.1 authorization server. The app sends the person to sign in. They choose a workspace and approve what the app may do, and the app gets a short-lived access token for the API.
* **Discovery:** `https://api.reminix.com/.well-known/oauth-authorization-server/auth` lists every endpoint. Apps can register themselves (dynamic client registration); registering grants nothing until a person approves.
* **Scopes:** an app asks for the same [scopes](#scopes), such as `workspace.audit:read`, plus `offline_access` for a refresh token. The person can give it fewer.
* **One person, one workspace:** a token acts for one person in one workspace, with that person’s role, so it needs no `X-Workspace` header. Request it for the resource `https://api.reminix.com/v1`.
* **Disconnecting:** the person can disconnect the app at any time under **Account → Connected apps**. Its tokens stop working on the next request.
## When authentication fails
[Section titled “When authentication fails”](#when-authentication-fails)
A missing, malformed, invalid or revoked credential gets a `401` with the standard [error envelope](/docs/errors/):
```json
{
"error": {
"code": "unauthorized",
"message": "Unauthorized",
"requestId": "…"
}
}
```
The response is the same whatever the cause, so it gives nothing away to someone guessing keys. Browser session cookies are never accepted on the public API.
# Command line
> Install the CLI, sign in, and call every API operation from a terminal or script.
The command line uses the same public API as your code. It acts as **you**, with a personal access token, in the workspaces you allow.
## Install
[Section titled “Install”](#install)
```bash
npm install -g @reminix/cli
```
## Sign in
[Section titled “Sign in”](#sign-in)
```bash
reminix login
```
Your browser opens. Sign in, choose which workspaces the command line may use and what it may do, and click **Allow**. The command line creates a personal access token for this computer, valid for 90 days and listed under **Account → API tokens**, and stores it in `~/.config/reminix/`.
On a server or in CI there’s no browser, so pass a token instead, or set it in the environment. Then it writes nothing to disk:
```bash
reminix login --token "$TOKEN" # or: echo "$TOKEN" | reminix login
REMINIX_TOKEN=pat_… reminix whoami
```
`reminix logout` revokes the token and forgets it.
## Choose a workspace
[Section titled “Choose a workspace”](#choose-a-workspace)
```bash
reminix workspace list
reminix workspace use acme # the default for later commands
reminix workspace audit-events list --workspace other-co # one call elsewhere
```
## Every API operation is a command
[Section titled “Every API operation is a command”](#every-api-operation-is-a-command)
Each operation in the [API reference](https://api.reminix.com/v1/docs) is a command, named after its resource and action:
```bash
reminix workspace webhook-endpoints list
reminix workspace webhook-endpoints create --url https://example.com/hooks
reminix workspace webhook-deliveries list we_123 --limit 10
reminix workspace webhook-endpoints delete we_123 --yes
```
* Ids in the path are arguments, and other inputs are flags. `reminix --help` lists them with descriptions.
* `--data '{…}'` sends a whole JSON body.
* Lists print one page. The last line gives the `--cursor` for the next.
* Writes send an [idempotency key](/docs/idempotency/) for you.
* An operation that can’t be undone asks for `--yes`.
* `reminix api ` calls any endpoint directly.
## Scripts and agents
[Section titled “Scripts and agents”](#scripts-and-agents)
Add `--json` to any command to get the API’s exact JSON on stdout. Errors then go to stderr as JSON too, in the API’s [error envelope](/docs/errors/) with the HTTP status:
```bash
reminix workspace webhook-endpoints list --json | jq '.items[].url'
```
```json
{
"error": {
"code": "forbidden",
"message": "…",
"status": 403,
"requestId": "…"
}
}
```
The exit code is `0` on success and `1` on any failure, including a refused API call or a missing flag.
# Errors & API rate limits
> The error envelope, the error codes you can rely on, and how fast each credential may call the API.
Every response outside the 2xx range uses the same envelope:
```json
{
"error": {
"code": "invalid_request",
"message": "Invalid request",
"requestId": "9f0c1c2e-…",
"docs": "https://reminix.com/docs/errors/#invalid_request",
"details": [{ "path": "name", "message": "Required" }]
}
}
```
| Field | What it is |
| ----------- | -------------------------------------------------------------------------------- |
| `code` | One of a small, stable set of codes. Branch on this. |
| `message` | A sentence for people. It may change, so don’t parse it. |
| `requestId` | Matches the `X-Request-Id` response header. Include it when you contact support. |
| `details` | Only on validation errors: one entry per field, with its `path` and `message`. |
| `docs` | A link to the code’s entry on this page. |
## Error codes
[Section titled “Error codes”](#error-codes)
We may add new codes over time. Treat a code you don’t recognise as `internal_error`.
### `unauthorized`
[Section titled “unauthorized”](#unauthorized)
`401`. The credential is missing, malformed, invalid, revoked or expired. Send `Authorization: Bearer ` with a working credential, and create a new one if it was revoked or has expired.
### `forbidden`
[Section titled “forbidden”](#forbidden)
`403`. The credential is valid but isn’t allowed to do this. Either it doesn’t have the endpoint’s [scope](/docs/authentication/#scopes), or your role in the workspace doesn’t allow it, or a personal access token can’t act in the workspace named by `X-Workspace`. Use a credential with the access it needs; the `message` names the missing scope.
### `not_found`
[Section titled “not\_found”](#not_found)
`404`. The resource doesn’t exist, or isn’t yours to see. Check the path and the id; the API reports an id from another workspace as not found.
### `method_not_allowed`
[Section titled “method\_not\_allowed”](#method_not_allowed)
`405`. The path exists, but not with this method. Use one of the methods in the `Allow` response header.
### `invalid_request`
[Section titled “invalid\_request”](#invalid_request)
`400`. The request didn’t pass validation. `details` lists each invalid field with its `path` and `message`.
### `rate_limited`
[Section titled “rate\_limited”](#rate_limited)
`429`. The credential has used its quota (see [API rate limits](#api-rate-limits)). Wait the number of seconds in `Retry-After`, then retry with backoff.
### `plan_limit_reached`
[Section titled “plan\_limit\_reached”](#plan_limit_reached)
`402`. The workspace has used its plan’s monthly allowance for what this request needs. The message says which allowance and when it resets, and the workspace’s owners got an email and a notification at 80% and 100%. Upgrade the plan under **Settings → Billing**, or wait for the month to reset; retrying sooner gets the same answer.
### `idempotency_key_reused`
[Section titled “idempotency\_key\_reused”](#idempotency_key_reused)
`409`. This `Idempotency-Key` was already used for a different request. Use a new key for a new operation, and reuse a key only to retry the same request (see [Idempotency](/docs/idempotency/)).
### `idempotency_in_progress`
[Section titled “idempotency\_in\_progress”](#idempotency_in_progress)
`409`. A request with the same `Idempotency-Key` is still running. Wait the number of seconds in `Retry-After`, then retry with the same key.
### `internal_error`
[Section titled “internal\_error”](#internal_error)
`500`. Something failed on our side. Retry with backoff, and if it keeps happening, contact support with the `requestId`.
## API rate limits
[Section titled “API rate limits”](#api-rate-limits)
Every authenticated request counts against a quota for its credential: per API key on the workspace endpoints, per token on `/v1/user/*`. When the quota runs out, the API answers:
```http
HTTP/1.1 429 Too Many Requests
Retry-After: 60
```
with `code: "rate_limited"`. Wait for `Retry-After`, then retry with backoff. Because limits are per credential, one busy integration doesn’t slow down another key’s requests.
# Finding scripts
> Search the team's catalog before writing something new, by words, tags or owner, and see who uses what.
The **Scripts** page is your team’s catalog. Before writing a new script, look there: someone may have built it already.
## Search and filter
[Section titled “Search and filter”](#search-and-filter)
* **Search** as you type. Words match the start of words in the name, slug, description and tags, so `rev mon` finds “Monthly revenue report”.
* **Tags:** click a tag to see everything with it.
* **Mine** and **Published only** narrow the list.
The filters are in the page’s address, so you can share a filtered list. You only ever see scripts you may use: a team’s script stays hidden from people outside the team, in search too.
From the API, and so from agents over MCP:
```bash
curl "https://api.reminix.com/v1/scripts?q=refund&tag=finance&published=true" \
-H "Authorization: Bearer $REMINIX_TOKEN" -H "X-Workspace: acme"
```
With `q`, the best matches come first (one page). Without it, the list is newest first and pages with `nextCursor`. `owner` takes a user id.
## Tags and README
[Section titled “Tags and README”](#tags-and-readme)
Set **tags** in `reminix.json` (`"tags": ["finance"]`), which replace the script’s tags when you publish a version, or in the script’s **Settings**. Up to 10, using lowercase letters, digits and dashes.
Reminix keeps the **README.md** in the folder you publish from with that version, and shows it on the script’s page while the version is published. Say what it does, when to use it, and what its inputs mean. It’s Markdown; raw HTML isn’t shown.
## Who uses it
[Section titled “Who uses it”](#who-uses-it)
Every script shows, for the last 30 days:
* its **runs**, the share that succeeded, and when it last ran;
* **where runs came from:** the app, the API, the command line, agents, schedules and triggers;
* the **people** who ran it most;
* its **schedules and triggers**, and the other scripts whose triggers run after it (on its `run.completed` or `run.failed`).
Before changing or retiring a script, check who would notice.
# Governance
> Decide who can use each script, which runs need a person's approval, and who publishes.
Every script has an owner: whoever created it. Its owner, or a workspace owner or admin, controls three things on its page, under **Settings**.
## Who can use it
[Section titled “Who can use it”](#who-can-use-it)
* **Everyone in the workspace,** the default.
* **Only these teams,** chosen from the workspace’s teams (Settings → Members). Members outside them don’t see the script at all: not in the app, the API, as an agent’s tool, or in its runs. Workspace owners and admins always see everything.
Guests see only what’s shared with a team they’re in.
Through the API: `PATCH /v1/scripts/{slug}` with `{ "access": "teams", "teamIds": ["…"] }` or `{ "access": "workspace" }`.
## Which runs need approval
[Section titled “Which runs need approval”](#which-runs-need-approval)
| Policy | What waits for a person |
| --------------------------------------- | ----------------------------------------------------- |
| **No approval** (the default) | Nothing. Runs start right away. |
| **Agents need approval** | Runs started by an AI agent (through the MCP server). |
| **Every run needs approval** (Business) | Every run, except by workspace owners and admins. |
A held run shows as **Awaiting approval**. Reminix notifies workspace owners and admins, and they decide on its approval page. If it’s approved, it runs the version it was asked for, with its inputs. If it’s denied, or not decided within a day, it’s **Cancelled**. Through the API, a held run answers `202` with the run and the request’s `approveUrl`; read the run (`GET /v1/runs/{id}`) to see how it ended. An agent’s tool says it’s waiting, with the link.
The policy is a setting of the script, never part of its code, so publishing new code can’t loosen it. If a workspace moves to a plan without every-run approval, a script that has it keeps it.
## Who publishes
[Section titled “Who publishes”](#who-publishes)
The script’s owner, or a workspace owner or admin, publishes a version. Two cases need a person’s approval instead:
* **An AI agent’s publish,** always. The request appears for a workspace owner or admin to approve.
* **Everyone’s publish,** when the workspace turns on **Settings → Scripts → Publishing → “Publishing needs a second person”** (Business). The publish becomes a request that another owner or admin approves, not the person who asked. If the person who asked approves it themselves, it’s refused; ask again for someone else.
Every change of access, policy and setting, and every approval, is in the workspace’s audit log.
# Give your agents safe tools
> Let Claude and other AI agents run your team's scripts as tools, with a person approving the ones that matter.
Every published script is also a tool for AI agents. Here’s how to connect an agent, and how to make sure it can’t take a real action without someone checking first.
## 1. Connect the agent
[Section titled “1. Connect the agent”](#1-connect-the-agent)
Add Reminix’s MCP server, `https://mcp.reminix.com/mcp`, to your agent. In Claude Code:
```bash
claude mcp add --transport http reminix https://mcp.reminix.com/mcp
```
In Claude, add it under **Settings → Connectors → Add custom connector**. The first time, your browser opens: sign in, choose one workspace, choose what the agent may do, and click **Allow**. The agent acts as you, with your access, and can be disconnected at any time under **Account → Connected apps**. See [MCP server](/docs/mcp/).
## 2. What the agent sees
[Section titled “2. What the agent sees”](#2-what-the-agent-sees)
Each published script you may use is a tool with the script’s own name (`refund-customer` is `refund_customer`), taking the script’s inputs. The agent can also search the catalog, read runs, and list secrets by name. It can’t save, change or read secrets.
## 3. Put a person in the loop
[Section titled “3. Put a person in the loop”](#3-put-a-person-in-the-loop)
For any script that takes a real action, open its **Settings** and set **Which runs need approval** to **Agents need approval**. Through the API:
```bash
curl -X PATCH https://api.reminix.com/v1/scripts/refund-customer \
-H "Authorization: Bearer $REMINIX_TOKEN" -H "X-Workspace: acme" \
-H "Content-Type: application/json" \
-d '{ "approvalPolicy": "agents" }'
```
Now when an agent calls the tool:
1. Reminix records the run as **Awaiting approval**, and the tool tells the agent it’s waiting, with a link.
2. Workspace owners and admins get a notification, and see exactly what will run, with which inputs.
3. Approved, it runs. Denied, or not decided within a day, it’s cancelled.
People on your team still run it straight away. On Business, you can also require approval for every run.
## 4. Publishing stays with people
[Section titled “4. Publishing stays with people”](#4-publishing-stays-with-people)
An agent can write and upload a script, and test-run its draft, but its publish is always a request that a person approves. See [Publish a script with your agent](/docs/agents-onboarding/).
Related: [Governance](/docs/governance/), [Approvals](/docs/approvals/).
# Alert when a run fails
> When a script's run fails, post the error to Slack automatically, with a link to the run.
Say the nightly sync fails now and then, and nobody notices until the morning. Here’s how to post an alert the moment it fails, using a trigger.
## 1. Write an alert script
[Section titled “1. Write an alert script”](#1-write-an-alert-script)
It posts a message to Slack, using a `SLACK_TOKEN` secret that may only be sent to `slack.com` (see [Send a report every week](/docs/guides/scheduled-report/) for saving it):
```json
{
"slug": "slack-alert",
"name": "Slack alert",
"entry": "index.ts",
"hosts": ["slack.com"],
"secrets": {
"SLACK_TOKEN": {
"host": "slack.com",
"header": "Authorization",
"value": "Bearer {secret}"
}
},
"inputSchema": {
"type": "object",
"required": ["text"],
"properties": {
"text": { "type": "string" },
"runId": { "type": "string" },
"error": { "type": "string" }
}
}
}
```
```ts
export default async function run(
inputs: { text: string; runId?: string; error?: string },
ctx,
) {
const lines = [inputs.text];
if (inputs.error) lines.push(`Error: ${inputs.error}`);
if (inputs.runId) lines.push(`https://app.reminix.com/runs/${inputs.runId}`);
const res = await fetch("https://slack.com/api/chat.postMessage", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ channel: "#alerts", text: lines.join("\n") }),
});
const answer = await res.json();
if (!answer.ok) throw new Error(`Slack: ${answer.error}`);
return { posted: true };
}
```
Publish it:
```bash
reminix scripts publish ./slack-alert --publish
```
## 2. Add the trigger
[Section titled “2. Add the trigger”](#2-add-the-trigger)
On the alert script’s page, choose **Triggers → Add trigger**: the event `run.failed`, only when `script.slug` equals `nightly-orders-sync`, with inputs taken from the event. Through the API:
```bash
curl -X POST https://api.reminix.com/v1/scripts/slack-alert/triggers \
-H "Authorization: Bearer $REMINIX_TOKEN" -H "X-Workspace: acme" \
-H "Content-Type: application/json" \
-d '{
"name": "Nightly sync failed",
"eventType": "run.failed",
"filter": { "script.slug": "nightly-orders-sync" },
"inputs": {
"text": "The nightly orders sync failed.",
"runId": "{{event.data.runId}}",
"error": "{{event.data.error}}"
}
}'
```
The alert runs within about a minute of the failure, as you, and Reminix records it on its page, marked **Trigger**.
## Good to know
[Section titled “Good to know”](#good-to-know)
* Chains stop after 3 triggered runs, so an alert that fails can’t set off an endless loop.
* On the sync’s page, under who uses it, the alert shows as a script that runs after it. Check there before changing or retiring the sync.
Related: [Schedules and triggers](/docs/schedules-and-triggers/), [Webhooks](/docs/webhooks/).
# Run a nightly sync
> Move a sync that takes minutes off someone's laptop, with progress you can watch, a cancel button, and a run every night.
Say a sync copies yesterday’s orders from one system to another, takes about ten minutes, and runs on one person’s laptop. Nobody else can see whether it ran, or why it failed. Long runs need the Team or Business plan.
## 1. Give it a longer timeout
[Section titled “1. Give it a longer timeout”](#1-give-it-a-longer-timeout)
In `reminix.json`, set `timeout` in seconds. The run lasts at most this long, or the plan’s longest run (10 minutes on Team, 15 on Business), whichever is shorter:
```json
{
"slug": "nightly-orders-sync",
"name": "Nightly orders sync",
"entry": "index.ts",
"timeout": 600,
"hosts": ["api.shop.example", "api.warehouse.example"],
"inputSchema": {
"type": "object",
"properties": { "date": { "type": "string", "title": "Day (YYYY-MM-DD)" } }
}
}
```
## 2. Report progress as it goes
[Section titled “2. Report progress as it goes”](#2-report-progress-as-it-goes)
```ts
export default async function run(inputs: { date?: string }, ctx) {
const day = inputs.date ?? yesterday();
const pages = await listOrderPages(day); // from api.shop.example
for (let i = 0; i < pages.length; i++) {
await copyPage(pages[i]); // to api.warehouse.example
ctx.progress((i + 1) / pages.length, `Page ${i + 1} of ${pages.length}`);
}
return { day, pages: pages.length };
}
```
`ctx.progress` drives the progress bar on the run’s page, and `ctx.log` lines appear there as they’re written.
## 3. Publish and try it
[Section titled “3. Publish and try it”](#3-publish-and-try-it)
```bash
reminix scripts publish ./nightly-orders-sync --publish
reminix scripts run nightly-orders-sync --input '{"date":"2026-10-04"}'
```
The run answers at once (`202`), and the command line follows it to the end. In the app, the run’s page shows the log and the progress, and a **Cancel run** button stops it.
## 4. Run it every night
[Section titled “4. Run it every night”](#4-run-it-every-night)
Add a schedule, for example every day at 2:00 in your timezone, with no inputs so it syncs yesterday. Then add a trigger for when it fails: see [Alert when a run fails](/docs/guides/alert-on-failure/).
## Good to know
[Section titled “Good to know”](#good-to-know)
* A long run is never retried automatically, so a sync that might stop halfway should be safe to run again for the same day.
* A run has up to 60 seconds of CPU time. Waiting on APIs doesn’t count, so a sync that mostly reads and writes over the network fits easily.
* Its minutes count toward the plan’s long-run minutes for the month.
Related: [Long runs](/docs/long-runs/), [Schedules and triggers](/docs/schedules-and-triggers/).
# Share a runbook with a team
> Turn an engineer's refund script into a form your support team can run, with the Stripe key kept out of reach.
Say an engineer has a script that refunds a Stripe payment, and the support team needs it several times a day. Today they ask in a channel and wait. Here’s how to give Support a form instead, without giving anyone the Stripe key.
## 1. Save the key as a secret
[Section titled “1. Save the key as a secret”](#1-save-the-key-as-a-secret)
A workspace owner or admin saves it once. It can only ever be sent to `api.stripe.com`:
```bash
printf %s "$STRIPE_KEY" | reminix secrets set STRIPE_KEY --hosts api.stripe.com
```
## 2. Write the script
[Section titled “2. Write the script”](#2-write-the-script)
```json
{
"slug": "refund-customer",
"name": "Refund a customer",
"description": "Refunds a Stripe payment in full.",
"entry": "index.ts",
"hosts": ["api.stripe.com"],
"secrets": {
"STRIPE_KEY": {
"host": "api.stripe.com",
"header": "Authorization",
"value": "Bearer {secret}"
}
},
"inputSchema": {
"type": "object",
"required": ["paymentId", "reason"],
"properties": {
"paymentId": { "type": "string", "title": "Payment ID (pi_…)" },
"reason": {
"type": "string",
"title": "Reason",
"enum": ["duplicate", "fraudulent", "requested_by_customer"]
}
}
}
}
```
```ts
export default async function run(
inputs: { paymentId: string; reason: string },
ctx,
) {
ctx.log(`refunding ${inputs.paymentId} (${inputs.reason})`);
const res = await fetch("https://api.stripe.com/v1/refunds", {
method: "POST",
body: new URLSearchParams({
payment_intent: inputs.paymentId,
reason: inputs.reason,
}),
});
const refund = await res.json();
if (!res.ok) throw new Error(refund.error?.message ?? "Stripe refused");
return { refund: refund.id, status: refund.status };
}
```
The input schema becomes the form: a text field for the payment and a list for the reason. Throwing an error makes the run fail with that message, so Support sees why.
## 3. Publish it
[Section titled “3. Publish it”](#3-publish-it)
```bash
reminix scripts publish ./refund-customer --publish
```
## 4. Share it with Support only
[Section titled “4. Share it with Support only”](#4-share-it-with-support-only)
On the script’s page, under **Settings → Who can use it**, choose **Only these teams** and pick Support. Through the API:
```bash
curl -X PATCH https://api.reminix.com/v1/scripts/refund-customer \
-H "Authorization: Bearer $REMINIX_TOKEN" -H "X-Workspace: acme" \
-H "Content-Type: application/json" \
-d '{ "access": "teams", "teamIds": [""] }'
```
Support now sees **Refund a customer** under Scripts, fills in the form, and clicks **Run**. Everyone else doesn’t see it at all.
## 5. If agents should use it too
[Section titled “5. If agents should use it too”](#5-if-agents-should-use-it-too)
Set **Which runs need approval** to **Agents need approval**. An agent can then start a refund, but it waits until an owner or admin approves it. See [Give your agents safe tools](/docs/guides/agent-tools/).
Every refund is on record, with who ran it, the inputs and Stripe’s answer, on the script’s page.
Related: [Scripts](/docs/scripts/), [Secrets](/docs/secrets/), [Governance](/docs/governance/).
# Send a report every week
> Post a weekly report to Slack on a schedule, so it arrives whether or not the person who wrote it is around.
Say someone pulls last week’s numbers every Monday morning and pastes them into Slack. Here’s how to make that a script that runs on a schedule.
## 1. Save the Slack token
[Section titled “1. Save the Slack token”](#1-save-the-slack-token)
A workspace owner or admin saves a Slack bot token that may post messages (`chat:write`), sendable only to `slack.com`:
```bash
printf %s "$SLACK_BOT_TOKEN" | reminix secrets set SLACK_TOKEN --hosts slack.com
```
## 2. Write the script
[Section titled “2. Write the script”](#2-write-the-script)
```json
{
"slug": "weekly-revenue",
"name": "Weekly revenue",
"entry": "index.ts",
"hosts": ["slack.com", "api.stripe.com"],
"secrets": {
"SLACK_TOKEN": {
"host": "slack.com",
"header": "Authorization",
"value": "Bearer {secret}"
},
"STRIPE_KEY": {
"host": "api.stripe.com",
"header": "Authorization",
"value": "Bearer {secret}"
}
},
"inputSchema": {
"type": "object",
"required": ["channel"],
"properties": { "channel": { "type": "string", "title": "Slack channel" } }
}
}
```
```ts
export default async function run(inputs: { channel: string }, ctx) {
const total = await lastWeeksRevenue(); // your query, through api.stripe.com
ctx.log(`last week: ${total}`);
const res = await fetch("https://slack.com/api/chat.postMessage", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
channel: inputs.channel,
text: `Revenue last week: ${total}`,
}),
});
const answer = await res.json();
if (!answer.ok) throw new Error(`Slack: ${answer.error}`);
return { posted: true, total };
}
```
Publish it, and run it once by hand to check the message:
```bash
reminix scripts publish ./weekly-revenue --publish
reminix scripts run weekly-revenue --input '{"channel":"#revenue"}' --log
```
## 3. Add the schedule
[Section titled “3. Add the schedule”](#3-add-the-schedule)
On the script’s page, choose **Schedules → Add schedule**: every Monday at 9:00, in your timezone, with `#revenue` as the channel. Or through the API:
```bash
curl -X POST https://api.reminix.com/v1/scripts/weekly-revenue/schedules \
-H "Authorization: Bearer $REMINIX_TOKEN" -H "X-Workspace: acme" \
-H "Content-Type: application/json" \
-d '{ "name": "Monday mornings", "cron": "0 9 * * 1",
"timezone": "Europe/London", "inputs": { "channel": "#revenue" } }'
```
The schedule runs as you, with your access. Each run is on the script’s page, marked **Schedule**. If a time is skipped, for example because the plan’s runs for the month are used up, the schedule’s card says why.
## 4. Hear about it when it fails
[Section titled “4. Hear about it when it fails”](#4-hear-about-it-when-it-fails)
Add a trigger on this script’s `run.failed`. See [Alert when a run fails](/docs/guides/alert-on-failure/).
Related: [Schedules and triggers](/docs/schedules-and-triggers/), [Secrets](/docs/secrets/).
# How Reminix works
> Where your code runs, what it can reach, how Reminix keeps secrets, how changes go live, and who can do what.
## Where code runs
[Section titled “Where code runs”](#where-code-runs)
Each version of a script runs in its own isolated sandbox. A run sees only its inputs: not other scripts, not your workspace’s data, not Reminix itself. When you publish, Reminix bundles the entry file and everything it imports (at most 5 MB), so a run never installs anything.
A run has up to 30 seconds. On Team and Business, a script can ask for longer, up to the plan’s limit (10 or 15 minutes); such a [long run](/docs/long-runs/) answers at once, shows its progress, and can be cancelled.
## What code can reach
[Section titled “What code can reach”](#what-code-can-reach)
A script has no network access until its version lists the hosts it may call. Then it can reach only those, over HTTPS, at most 50 requests a run. Reminix writes every request to the run’s log. Anything else fails with “Host not allowed”.
## Secrets
[Section titled “Secrets”](#secrets)
You save an API key once. Reminix encrypts it with a key for your workspace and never shows it again. Each secret lists the hosts it may be sent to. When a script’s request goes to one of those hosts, Reminix adds the secret (as a header such as `Authorization: Bearer …`) on the way out. The code doesn’t hold the value, so it can’t log it or send it anywhere else. See [Secrets](/docs/secrets/).
## How changes go live
[Section titled “How changes go live”](#how-changes-go-live)
Uploading code makes a **draft** version. Nothing changes for your team until a version is **published**, by the script’s owner or a workspace owner or admin. An AI agent’s publish always waits for a person, and a workspace can require a second person for every publish. Rolling back is publishing an earlier version. A draft can be test-run before anyone publishes it.
## Who can do what
[Section titled “Who can do what”](#who-can-do-what)
* **Who sees and runs a script:** everyone in the workspace, or only chosen teams. Others don’t see it at all, in the app, the API or as an agent’s tool.
* **Which runs wait for approval:** none, those started by AI agents, or every run (Business). An owner or admin approves or denies; Reminix cancels a run that nobody decides within a day.
* **Who manages secrets:** workspace owners and admins. Members see their names and hosts, never their values. Agents can’t save or change them.
These are settings on the script, never part of its code, so publishing new code can’t loosen them. See [Governance](/docs/governance/).
## Every run on record
[Section titled “Every run on record”](#every-run-on-record)
For each run, Reminix records its inputs, the version it used, and who started it from where: the app, the API, the command line, an agent, a schedule or a trigger. It also records how the run ended, its output or error, and its log. Reminix keeps logs for your plan’s period (7, 30 or 90 days). Changes to access, policies, secrets and publishing are in the workspace’s audit log.
## Retries
[Section titled “Retries”](#retries)
Reminix marks an interrupted run failed, and never retries it automatically, because a retry could repeat what it did, such as sending an email twice. Starting a run is safe to retry with an [idempotency key](/docs/idempotency/).
# Idempotency
> Retry writes safely with the Idempotency-Key header.
A request can time out after the API has already acted on it. To retry a write without doing it twice, send an `Idempotency-Key` header: any unique string of up to 255 printable ASCII characters, such as a UUID you generate for each operation.
```bash
curl -X POST \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: 6f1c2e9a-8b3d-4c47-9a51-0d2f7e4b8c13" \
-H "Content-Type: application/json" \
-d '{ … }' \
https://api.reminix.com/v1/…
```
The first request with a key runs normally. A retry with the same key and the same request gets the first response again, with the same status and body, and doesn’t run the operation a second time. A replayed response carries the header `Idempotency-Replayed: true`.
## The rules
[Section titled “The rules”](#the-rules)
* **Writes only.** The header applies to `POST`, `PUT`, `PATCH` and `DELETE`. Reads are safe to repeat anyway, and ignore it.
* **One key per request.** Using a key again for a different request, with a different endpoint or body, fails with a `409` and the code `idempotency_key_reused`. Generate a new key for each operation, and reuse it only to retry that operation.
* **A retry during the first request waits.** If the first request is still running, the retry gets `409 idempotency_in_progress` and a `Retry-After` header. Retry after that many seconds.
* **You can retry server errors.** A `5xx` response isn’t stored, so a retry with the same key runs the request again. Any other response, a success or a `4xx`, is what every retry gets back.
* **Per credential, for 24 hours.** A key belongs to the API key or token that sent it, so two integrations never collide, and it’s kept for 24 hours.
Sending a key is optional. We recommend it on every write.
# Long runs
> Scripts that take minutes. They answer at once, show their progress, and can be cancelled.
By default a run must finish within 30 seconds, and whoever starts it waits for the result. For reports, data syncs and batch jobs, give the script a longer `timeout`, in seconds, in `reminix.json`:
```json
{
"slug": "nightly-sync",
"name": "Nightly sync",
"entry": "index.ts",
"timeout": 600,
"inputSchema": { "type": "object", "properties": {} }
}
```
Long runs are part of the Team and Business plans. A long run lasts at most the version’s `timeout` or the plan’s longest run, whichever is shorter: 10 minutes on Team, 15 on Business. Its minutes count toward the plan’s long-run minutes for the month (1,000 on Team, 10,000 on Business). See [Plans and limits](/docs/plans/).
## What changes
[Section titled “What changes”](#what-changes)
**It answers at once.** Starting a run returns `202` with the run, its status `running`. In the app you land on the run’s page. The command line (`reminix scripts run`) follows it to the end. An agent gets the run’s id and reads it later (`runs.get`).
**You can see it working.** The run’s page shows its log as it grows, and a progress bar when the code reports progress:
```ts
export default async function run(inputs, ctx) {
for (let i = 0; i < pages.length; i++) {
await sync(pages[i]);
ctx.log(`synced page ${i + 1}`);
ctx.progress((i + 1) / pages.length, `Page ${i + 1} of ${pages.length}`);
}
return { pages: pages.length };
}
```
Through the API, `GET /v1/runs/{id}` includes `progress` and `progressMessage`, and `GET /v1/runs/{id}/logs` returns the log so far.
**You can stop it.** Use **Cancel run** on its page, `POST /v1/runs/{id}/cancel`, or `reminix runs cancel `. Whoever started it, the script’s owner, and workspace owners and admins can cancel. The code stops, and the run ends `cancelled`.
## Good to know
[Section titled “Good to know”](#good-to-know)
* A long run executes at most once. It’s never retried automatically, because a retry could repeat what it did, such as sending an email. If it stops unexpectedly, it’s marked **failed: interrupted**.
* It has up to 60 seconds of CPU time, which is plenty for code that mostly waits on APIs. Split heavy computation into steps.
* A workspace runs at most 10 long runs at once. Reminix refuses the 11th with `429` until one finishes.
* Everything else works as for any run: the network rules, secrets, approvals, schedules and triggers, test runs of drafts, and the `run.completed` and `run.failed` events, sent when it ends.
# MCP server
> Connect Claude or any MCP client to your workspace, what it can do there, and how you stay in control.
AI agents that support the Model Context Protocol (MCP), such as Claude and many other assistants and coding agents, can work in your workspace through our MCP server:
```text
https://mcp.reminix.com/mcp
```
## Connect
[Section titled “Connect”](#connect)
Add the address above as a remote MCP server. In Claude, that’s **Settings → Connectors → Add custom connector**. The first time, your browser opens:
1. Sign in, if you aren’t already.
2. Choose the **one workspace** the agent will work in.
3. Choose what it may do. Everything that can change data starts unticked, so tick only what the agent needs.
4. Click **Allow**.
There’s nothing to copy and no key to store. The agent gets its own sign-in, valid only for that workspace and that access.
## What the agent can do
[Section titled “What the agent can do”](#what-the-agent-can-do)
The agent’s tools are the [API](https://api.reminix.com/v1/docs) operations you allowed, such as reading the workspace or listing webhook deliveries. It always acts as **you**, with your role in the workspace: if you couldn’t do something in the app, the agent can’t either. Everything it changes appears in the workspace’s audit log under your name.
Each published script you may use is a tool too, named after the script (`refund-customer` is `refund_customer`) and taking its inputs.
Some actions are never available to agents, even with full access: anything that sends your workspace’s data somewhere new or gives out access, such as creating webhook endpoints or credentials. The agent can **ask** for them instead. You approve or deny in the app, and if the action creates a secret, the app shows it to you, never to the agent. See [Approvals](/docs/approvals/).
## Staying in control
[Section titled “Staying in control”](#staying-in-control)
* **Account → Connected apps** lists every agent you’ve connected. Disconnect one and it stops working at once.
* The workspace’s **audit log** records what each agent did.
* If you’re removed from the workspace, your agents lose access too.
## For developers
[Section titled “For developers”](#for-developers)
The server follows the MCP authorization specification. A request without a token gets a `401` pointing to `/.well-known/oauth-protected-resource/mcp`, which names the authorization server: `https://api.reminix.com/auth`, with dynamic client registration and PKCE. The authorization server issues tokens for the MCP server only.
For your own scripts, the [command line](/docs/cli/) or the [SDK](/docs/sdk/) are simpler. See [Use with AI agents](/docs/agents/) for the agent skill.
# Pagination
> How list endpoints return results a page at a time.
List endpoints return results newest first, a page at a time. Each page comes with a cursor that points to the next one.
```bash
curl \
-H "Authorization: Bearer YOUR_API_KEY" \
"https://api.reminix.com/v1/workspace/audit-events?limit=25"
```
```json
{
"items": [{ "id": "…", "action": "member.invited", "createdAt": "…" }],
"nextCursor": "eyJ0IjoiMjAyNi0wOC0uLi4ifQ"
}
```
To get the next page, send the cursor back as it is:
```bash
curl \
-H "Authorization: Bearer YOUR_API_KEY" \
"https://api.reminix.com/v1/workspace/audit-events?limit=25&cursor=eyJ0IjoiMjAyNi0wOC0uLi4ifQ"
```
Keep going until `nextCursor` is `null`. That’s the last page.
## Good to know
[Section titled “Good to know”](#good-to-know)
* `limit` is from 1 to 100, and 25 by default.
* Don’t build or change a cursor yourself. A cursor that isn’t one we gave you returns `invalid_request`.
* Rows added while you’re paging don’t cause skipped or repeated items.
* Items may gain new fields over time. We won’t rename existing fields.
# Plans and limits
> What each plan includes, who counts as an author, and what happens when you reach a limit.
| | Free | Team | Business | Enterprise |
| ------------------- | ------------ | --------------------------------------- | ----------------------------------------- | ---------- |
| Price | $0 | $20 per author a month | $50 per author a month (from $400) | Talk to us |
| Authors | 3 | Any number | Any number | Any number |
| People who only run | 10 | 25 included | Any number | Any number |
| Runs a month | 1,000 | 10,000 | 100,000 | Custom |
| Long runs | No | Up to 10 minutes; 1,000 minutes a month | Up to 15 minutes; 10,000 minutes a month | Custom |
| Approvals | Agents’ runs | Agents’ runs | Also every run, and second-person publish | Custom |
| Run logs kept | 7 days | 30 days | 90 days | Custom |
## Who counts as an author
[Section titled “Who counts as an author”](#who-counts-as-an-author)
An author is anyone who uploaded or published a script version this month. They’re counted once, however often they publish. People who only run scripts, from the app, the API, the command line or agents, aren’t authors. Your usage this month is in **Settings → Billing**.
## When you reach a limit
[Section titled “When you reach a limit”](#when-you-reach-a-limit)
At 80% and 100% of a monthly allowance, your workspace’s owners get an email and a notification, and the app shows a notice. At the limit, Reminix refuses clearly, with `402 plan_limit_reached` and the date the limit resets:
* **Runs:** Reminix refuses a new run. One that’s already running finishes. Schedules and triggers skip that time, their card says why, and they start again next month or after an upgrade.
* **Authors:** on Free, a 4th person can’t upload or publish this month. The three who already have can.
* **Long runs:** these need Team or Business. Each lasts at most the plan’s longest run (10 or 15 minutes), and its minutes count toward the month’s allowance.
* **Approvals:** every-run approval and second-person publishing are Business features. Once turned on, they stay on if the plan changes, so a downgrade never makes a script less safe.
# Quickstart
> Publish your first script and run it, in about five minutes.
By the end of this page you’ll have a script that greets someone by name, published in your workspace, and you’ll have run it from the command line and the app.
## 1. Create a workspace
[Section titled “1. Create a workspace”](#1-create-a-workspace)
[Sign up](https://app.reminix.com/sign-up), verify your email and create a workspace. Scripts, secrets and runs all belong to a workspace.
## 2. Install the command line and sign in
[Section titled “2. Install the command line and sign in”](#2-install-the-command-line-and-sign-in)
```bash
npm install -g @reminix/cli
reminix login
```
Your browser opens: choose your workspace, allow the command line to manage scripts and runs, and click **Allow**.
## 3. Write the script
[Section titled “3. Write the script”](#3-write-the-script)
```bash
mkdir hello && reminix init --dir hello
```
This puts a `reminix.json`, a `README.md` and an entry file (`index.ts`) in the folder. Make the entry file say hello:
hello/index.ts
```ts
export default async function run(inputs: { name: string }, ctx) {
ctx.log(`greeting ${inputs.name}`);
return { message: `Hello, ${inputs.name}!` };
}
```
And describe its input in `reminix.json`:
```json
{
"slug": "hello",
"name": "Say hello",
"entry": "index.ts",
"inputSchema": {
"type": "object",
"required": ["name"],
"properties": { "name": { "type": "string", "title": "Name" } }
}
}
```
## 4. Publish it
[Section titled “4. Publish it”](#4-publish-it)
```bash
reminix scripts publish ./hello --publish
```
The first publish creates the script and makes version 1 live. Without `--publish`, it would upload a draft that nobody else can run yet.
## 5. Run it
[Section titled “5. Run it”](#5-run-it)
From the command line:
```bash
reminix scripts run hello --input '{"name":"Ada"}' --log
```
```json
{ "message": "Hello, Ada!" }
```
In the app, open **Scripts → Say hello**: the input you described is a form with a **Run** button, and the run you just made is in its history. Through the API it’s `POST /v1/scripts/hello/runs`, and to an AI agent connected over [MCP](/docs/mcp/) it’s a tool called `run_hello`.
Next, read [Scripts](/docs/scripts/) to call APIs with your secrets, or pick a [guide](/docs/guides/runbooks/) close to what you’re building.
# Runs
> Run a published script from the API or the command line, and read its output and log.
A run is one execution of a script’s published version. Reminix records every run: its inputs, the version it used, and who started it from where (the app, the API, the command line or an agent). It also records how the run ended, its output or error, and its log.
## Running a script
[Section titled “Running a script”](#running-a-script)
In the app, open the script, fill in its form and click **Run**. From the command line, `reminix scripts run --input ''`. Agents connected through the [MCP server](/docs/mcp/) see a tool for each published script, named after it (`refund_customer`). Through the API, with a key or token that has `runs:write`:
```bash
curl -X POST https://api.reminix.com/v1/scripts/refund-customer/runs \
-H "Authorization: Bearer $REMINIX_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "inputs": { "orderId": "o_123" } }'
```
The answer comes when the run finishes:
```json
{
"id": "run_7b1a…",
"script": { "id": "cap_3f0c…", "slug": "refund-customer" },
"version": 3,
"status": "succeeded",
"inputs": { "orderId": "o_123" },
"output": { "refunded": "o_123" },
"error": null,
"interface": "api",
"logLines": 1,
"durationMs": 42
}
```
A run that throws ends with `"status": "failed"` and its `error`. That’s still a `200`, because the run happened. Read what it logged with `GET /v1/runs/{id}/logs`.
## Reminix checks inputs first
[Section titled “Reminix checks inputs first”](#reminix-checks-inputs-first)
The inputs must match the version’s `inputSchema`. If they don’t, the run doesn’t start, and you get `400 invalid_request` with one entry per problem in `details`:
```json
{
"error": {
"code": "invalid_request",
"message": "The inputs do not match the script's input schema",
"details": [
{
"path": "/orderId",
"message": "Instance type \"number\" is invalid. Expected \"string\"."
}
]
}
}
```
## Where the code runs
[Section titled “Where the code runs”](#where-the-code-runs)
Each version runs in its own sandbox, with no access to other scripts or to your workspace, only its inputs. It has no network access unless its version declares `hosts`. Then it can reach only those, over HTTPS, at most 50 requests a run, with its secrets attached by Reminix ([Scripts → Calling APIs](/docs/scripts/#calling-apis)). Every request appears in the run’s log.
A run can take up to 30 seconds. On Team and Business, a script can ask for longer: see [Long runs](/docs/long-runs/).
If a run can’t use one of its secrets, it fails before it starts and says why. The secret may not be set, may not be allowed to go to that host, or may not be readable by code.
## History and usage
[Section titled “History and usage”](#history-and-usage)
`GET /v1/runs` lists runs, newest first (add `?script=` for one script). `GET /v1/runs/{id}` reads one.
Every run counts toward your plan’s runs for the month: 1,000 on Free, 10,000 on Team, 100,000 on Business. Reminix counts a run when it starts, and never counts one it refuses. Past the limit, Reminix refuses a new run with `402 plan_limit_reached` until the month resets or you upgrade. See [Plans and limits](/docs/plans/).
Webhook events: `run.completed` and `run.failed`, and `script.published` when a new version goes live.
# Schedules and triggers
> Run a script on a timetable, or when something happens in your workspace.
A script can run without anyone clicking Run: on a **schedule**, or on a **trigger**. Either way, Reminix records the run like any other, marked **Schedule** or **Trigger**. It acts as the person who created the schedule or trigger, with their access, and counts toward the workspace’s runs.
## Schedules
[Section titled “Schedules”](#schedules)
On the script’s page, choose **Schedules → Add schedule**. Give it a name and say when to run: every hour, every day at 9:00, weekdays at 9:00, every Monday, or any 5-field cron. Then choose a timezone, and fill in the inputs with the script’s own form. Runs are at least 5 minutes apart and follow the timezone’s clock, including daylight saving time.
```bash
curl -X POST https://api.reminix.com/v1/scripts/nightly-report/schedules \
-H "Authorization: Bearer $REMINIX_TOKEN" -H "X-Workspace: acme" \
-H "Content-Type: application/json" \
-d '{ "name": "Weekday mornings", "cron": "0 9 * * 1-5",
"timezone": "Europe/Berlin", "inputs": { "team": "sales" } }'
```
## Triggers
[Section titled “Triggers”](#triggers)
Choose **Triggers → Add trigger**. Pick the event: any workspace event, such as another script’s `run.completed` or `run.failed`, `script.published`, or a member joining. Optionally, run only when a field of the event equals a value. Then set the inputs. A value written as `{{event.data.}}` comes from the event:
```json
{
"name": "Alert on failure",
"eventType": "run.failed",
"filter": { "script.slug": "nightly-report" },
"inputs": { "runId": "{{event.data.runId}}", "error": "{{event.data.error}}" }
}
```
A trigger starts its run within about a minute of the event. A chain of triggered runs (a run whose completion triggers another, and so on) stops after 3, so a script can never trigger itself forever.
## Who can, and when they stop
[Section titled “Who can, and when they stop”](#who-can-and-when-they-stop)
* Anyone who may run a script can schedule or trigger it, as themselves. Its creator, or a workspace owner or admin, can change or delete it. For a script whose every run needs approval, only owners and admins can, since they’re the approvers.
* A person creates schedules and triggers, in the app or with a person’s token. An AI agent can list them, but asks you to create them. A workspace API key can’t create them, because there’s no person to act as.
* If its creator leaves the workspace or loses access to the script, Reminix switches the schedule or trigger off, and its card says why. Switch it back on, or recreate it, as someone who has access.
* If Reminix refuses a run before it starts, it skips that time and the schedule stays on. Its card shows the last time it was skipped and why: for example, no published version, inputs that no longer fit a new version, or the plan’s runs for the month used up.
# Scripts
> Publish a script your team, or an agent, wrote, so the whole team can run it safely.
A script in Reminix is code your team can run safely, from the app, the API or an AI agent. It has a name, an owner, and the JSON Schema of its inputs, which becomes the form people fill in and the input agents send. Its code lives in **versions**: every change is a new version, and runs use the one that’s **published**.
## The folder: `reminix.json` and an entry file
[Section titled “The folder: reminix.json and an entry file”](#the-folder-reminixjson-and-an-entry-file)
```json
{
"slug": "refund-customer",
"name": "Refund a customer",
"description": "Refunds an order and notifies the customer.",
"entry": "index.ts",
"inputSchema": {
"type": "object",
"required": ["orderId"],
"properties": { "orderId": { "type": "string" } }
}
}
```
```ts
// index.ts: the entry. It may import other files and npm packages.
import { refund } from "./payments.ts";
export default async function run(inputs: { orderId: string }, ctx) {
ctx.log(`refunding ${inputs.orderId}`);
return { refunded: inputs.orderId };
}
```
The entry default-exports `run(inputs, ctx)` and returns JSON. JavaScript and TypeScript both work. When you publish, Reminix bundles the entry and everything it imports into one module of at most 5 MB: your other files, and npm packages from the folder’s `node_modules` (run `npm install` first). Node built-ins such as `node:crypto` and `node:buffer` can be imported.
Add `"tags": ["finance", "refunds"]` to `reminix.json` (up to 10, lowercase) and a `README.md` beside it. The script’s page shows the README, and both help your team [find it](/docs/finding-scripts/).
## Calling APIs
[Section titled “Calling APIs”](#calling-apis)
By default a script has no network. To call an API, list its hosts, and the secrets to attach to requests to them:
```json
{
"hosts": ["api.stripe.com"],
"secrets": {
"STRIPE_KEY": {
"host": "api.stripe.com",
"header": "Authorization",
"value": "Bearer {secret}"
}
}
}
```
```ts
export default async function run(inputs, ctx) {
// No key in the code: Reminix adds the Authorization header on the way out.
const res = await fetch("https://api.stripe.com/v1/refunds", {
method: "POST",
body: new URLSearchParams({ payment_intent: inputs.paymentId }),
});
return await res.json();
}
```
* Requests go out only to `hosts`, over HTTPS. Anything else fails with “Host not allowed”, and every request is in the run’s log.
* Reminix attaches each secret only to its `host`, and only if the secret itself allows that host ([Secrets](/docs/secrets/)). The code never holds the value. One caution: an API that echoes your credentials back in its response would reveal them to the code, so don’t send secrets to one.
* `value` defaults to `{secret}`. Use it to add a scheme, as in `"Bearer {secret}"`.
* To read a secret as a plain value (`ctx.env.NAME`), list it in `"env": ["NAME"]`. That’s allowed only for a secret whose owner turned on “Let scripts read the value itself”.
The script’s page shows each version’s hosts and secrets, so whoever publishes can see what it can reach.
## Publishing
[Section titled “Publishing”](#publishing)
```bash
reminix scripts publish ./refund-customer # uploads a draft version
reminix scripts publish ./refund-customer --publish # and publishes it
```
The first publish creates the script. A draft changes nothing until a version is published, by the script’s owner or a workspace owner or admin. An agent’s publish becomes a request that a person approves. Publishing an older version again rolls back. See [Governance](/docs/governance/) for who can use a script, runs that need approval, and a second person for publishing.
Through the API, with a key or token that has `scripts:write`: `POST /v1/scripts` creates one, `POST /v1/scripts/{slug}/versions` uploads a version (`{ entry, inputSchema, code }`), and `POST /v1/scripts/{slug}/versions/{number}/publish` publishes it.
## Running it
[Section titled “Running it”](#running-it)
Once a version is published, anyone in the workspace who may use the script can run it:
* **In the app:** open it under Scripts. Its input schema is a form, and **Run** shows the output and the log.
* **From the command line:** `reminix scripts run refund-customer --input '{"orderId":"o_123"}'` prints the output. Add `--log` to print the log too.
* **From an agent:** through the [MCP server](/docs/mcp/), every published script is a tool with the script’s own name (`refund_customer`) that takes its inputs.
* **From the API:** `POST /v1/scripts/{slug}/runs`. See [Runs](/docs/runs/).
# TypeScript SDK
> Call the API from TypeScript, with types generated from the API itself.
The SDK is a small typed client. Every path, parameter and response takes its type from the same definitions that validate requests, so it always matches the [API reference](https://api.reminix.com/v1/docs).
## Install
[Section titled “Install”](#install)
```bash
npm install @reminix/sdk
```
## Make a call
[Section titled “Make a call”](#make-a-call)
```ts
import { createClient, unwrap } from "@reminix/sdk";
const api = createClient({ token: process.env.API_KEY! });
const endpoints = unwrap(await api.GET("/workspace/webhook-endpoints"));
for (const endpoint of endpoints.items) console.log(endpoint.url);
```
* With an **API key**, every call acts in the key’s workspace.
* With a **personal access token**, name the workspace: `createClient({ token, workspace: "acme" })`.
* `unwrap` returns the data, or throws an `ApiError` with the [error envelope](/docs/errors/)’s `code`, `message`, `status` and `requestId`.
* Writes can send an [idempotency key](/docs/idempotency/): `api.POST("/workspace/webhook-endpoints", { body, headers: { "idempotency-key": id } })`.
# Secrets
> Store the API keys your scripts use. Encrypted, write-only, and sent only where you allow.
A secret is an API key or token your scripts use, such as a Stripe key or a Slack token. Reminix keeps it so the code never has to.
* **Write-only.** You save a value, and nothing shows it again: not the app, the API, the command line or an agent. To rotate it, save a new value.
* **Encrypted at rest,** with a key for each workspace.
* **Sent only to its hosts.** Each secret lists the hosts it may be sent to, such as `api.stripe.com`. Reminix attaches it to a script’s requests to those hosts, and to nothing else, whatever the code says.
* **Not readable by code,** unless you turn on “Let scripts read the value itself” for that secret.
Owners and admins save, rotate and delete secrets. Members see their names and hosts, which they need to write manifests, but never their values.
## Saving one
[Section titled “Saving one”](#saving-one)
In the app, go to **Settings → Secrets → Add secret**. On the command line, the value comes from standard input, never from an argument, which would end up in your shell history:
```bash
printf %s "$STRIPE_KEY" | reminix secrets set STRIPE_KEY --hosts api.stripe.com
```
Through the API, with a key that has `secrets:write`:
```bash
curl -X PUT https://api.reminix.com/v1/secrets/STRIPE_KEY \
-H "Authorization: Bearer $REMINIX_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "value": "sk_live_…", "hosts": ["api.stripe.com"] }'
```
`GET /v1/secrets` lists names and hosts, and `DELETE /v1/secrets/{name}` deletes one. Agents can list secrets but never save or delete them. An agent that needs a secret asks you to add it.
## Using one
[Section titled “Using one”](#using-one)
A script declares the secrets it uses in its `reminix.json`. See [Scripts → Calling APIs](/docs/scripts/#calling-apis). A run uses a secret only if the secret allows the host it’s sent to. After a rotation, the next run uses the new value.
# Versioning
> What can change in the API, what never changes, and how we announce deprecations.
The API’s version is in its path: `/v1`. Within a version, we only make changes that don’t break a correctly written integration. The same promise covers the command line and the tools agents use, because their names come from the API’s operations.
## Changes we may make at any time
[Section titled “Changes we may make at any time”](#changes-we-may-make-at-any-time)
* New endpoints.
* New optional request parameters and fields.
* New fields in responses.
* New error codes, webhook event types and scopes.
* New commands, and new tools for agents.
Write your integration so these don’t break it:
* Ignore response fields you don’t recognise.
* Treat an error `code` you don’t recognise as `internal_error`, and branch on `code`, never on `message`.
* Treat cursors and ids as opaque strings.
## Changes we never make within `/v1`
[Section titled “Changes we never make within /v1”](#changes-we-never-make-within-v1)
* Removing or renaming an endpoint, a field or an operation, and so a command or an agent’s tool.
* Changing the type or meaning of an existing field.
* Making an optional parameter required.
* Changing how requests authenticate, or which error a given failure returns.
A change like these only ships in a new version, `/v2`, which runs alongside `/v1` while integrations move over.
## Deprecations
[Section titled “Deprecations”](#deprecations)
Before we retire anything in `/v1`, we announce it in the changelog and mark it **deprecated** in the [API reference](https://api.reminix.com/v1/docs), with what to use instead. Deprecated endpoints keep working for as long as `/v1` exists.
# Webhooks
> Receive workspace events at your endpoint, verify signatures, and handle retries.
Webhooks send workspace events to an HTTPS endpoint you host, as they happen. Workspace owners manage endpoints in the app under **Settings → Webhooks**: add a URL, copy the signing secret (it’s shown once), and events start arriving.
## Managing endpoints through the API
[Section titled “Managing endpoints through the API”](#managing-endpoints-through-the-api)
Software can manage endpoints too, with a credential that has the `workspace.webhooks:write` scope. Reading them needs `workspace.webhooks:read`, and a personal access token must also belong to a workspace owner.
```bash
curl -X POST \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/hooks" }' \
https://api.reminix.com/v1/workspace/webhook-endpoints
```
The response includes the endpoint’s signing secret, once. The API can also list endpoints, re-enable one that we switched off after repeated failures, send a test event, and list and redeliver deliveries. See the [API reference](https://api.reminix.com/v1/docs).
AI agents connected to the workspace can read endpoints and delivery history, send test events and redeliver. They can’t add, remove or re-enable endpoints, because that changes where your workspace’s data goes, so a person does it.
## A delivery
[Section titled “A delivery”](#a-delivery)
Each delivery is an HTTP `POST` with a JSON body:
```json
{
"id": "evt_5f0c…",
"type": "workspace.member.joined",
"createdAt": "2026-08-20T12:00:00.000Z",
"data": { "userId": "…", "email": "casey@example.com" }
}
```
and three signature headers:
```http
webhook-id: del_9a1b…
webhook-timestamp: 1755691200
webhook-signature: v1,MEQCIB…
```
`webhook-id` identifies the delivery, and stays the same when it’s retried, so use it to ignore duplicates. The `id` in the body identifies the event. If you have several endpoints, each gets its own delivery of the same event; to process an event once across endpoints, deduplicate by its `id`.
## Verifying signatures
[Section titled “Verifying signatures”](#verifying-signatures)
We sign deliveries with your endpoint’s secret (`whsec_…`) in the same way as [Svix](https://docs.svix.com/receiving/verifying-payloads/how), so any standard Svix library can verify them:
```ts
import { Webhook } from "svix";
const wh = new Webhook(process.env.WEBHOOK_SECRET);
// Express-style handler; `payload` must be the RAW request body string.
app.post("/webhooks", (req, res) => {
let event;
try {
event = wh.verify(req.body, req.headers);
} catch {
return res.status(400).send("bad signature");
}
// handle event…
res.status(200).send("ok");
});
```
To verify by hand: the signature is `v1,` followed by a Base64 HMAC-SHA256 of `` `${webhookId}.${timestamp}.${body}` ``, keyed with the secret after its `whsec_` prefix, Base64-decoded. Always verify the **raw** body, because re-serialising the JSON changes it and breaks the signature. Reject timestamps more than a few minutes old, so nobody can replay an old delivery.
## Respond quickly, process later
[Section titled “Respond quickly, process later”](#respond-quickly-process-later)
Answer with a `2xx` within 10 seconds. If processing takes longer, acknowledge first and do the work afterwards, because a timeout counts as a failed delivery.
## Retries and failures
[Section titled “Retries and failures”](#retries-and-failures)
We retry a failed delivery (anything but a `2xx`, or a timeout) automatically, with backoff, up to 6 attempts, with the same `webhook-id`. If an endpoint fails 20 deliveries in a row, we switch it off. Once your endpoint is working again, delete it and add it back, which gives it a new secret.
Under **Settings → Webhooks → Deliveries** you can see each delivery’s status and attempts, send a test event (`type: "ping"`), and redeliver any recorded delivery. A redelivery has a new `webhook-id` but the same event `id`, so deduplicating by event still works.
## Events
[Section titled “Events”](#events)
| Type | Sent when | `data` |
| ------------------------- | ------------------------------ | ----------------- |
| `workspace.member.joined` | Someone accepts an invitation. | `userId`, `email` |
| `ping` | You send a test from Settings. | a test message |
Event payloads only ever gain fields, so write parsers that ignore fields they don’t know. `GET /v1/workspace/event-types` lists every event type and what it means.
## Choosing events
[Section titled “Choosing events”](#choosing-events)
An endpoint receives every event unless you choose otherwise. In **Settings → Webhooks**, pick “Only these” when you add it, or pass `eventTypes` when you create it through the API:
```bash
curl -X POST https://api.reminix.com/v1/workspace/webhook-endpoints \
-H "Authorization: Bearer YOUR_API_KEY" -H "content-type: application/json" \
-d '{"url":"https://example.com/hooks","eventTypes":["workspace.member.joined"]}'
```
A test event (`ping`) always reaches the endpoint you’re testing.
# Workspace usage
> How much of your plan the workspace used this month, and its limits.
We count some things per calendar month (UTC). Each count, called a meter, comes with your plan’s limit for it. See them under **Settings → Billing**, or through the API:
```bash
curl -H "Authorization: Bearer YOUR_API_KEY" \
https://api.reminix.com/v1/workspace/usage
```
```json
{
"periodStart": "2026-10-01T00:00:00.000Z",
"periodEnd": "2026-11-01T00:00:00.000Z",
"meters": [
{
"id": "webhook_deliveries",
"description": "Webhook deliveries this month (each event, each endpoint)",
"used": 1520,
"limit": null
}
]
}
```
`limit` is `null` when the plan has no limit for that meter. Counts start again at the beginning of each month. The key needs the `workspace:read` scope.