Ogen
In the docs
On this page

Docs

Connect an agent, look up a tool, run your tenant.

What Ogen does

Your agent asks Ogen before it acts. Ogen reads the fact the action depends on live from your own system, whether a database, a CRM, an ERP or a spreadsheet, at the moment the agent asks. When the sources agree, the agent goes ahead and your own system does the work. When they don't, or the action is costly to undo, it waits for a named person, who decides in one click, on the record. Ogen never guesses, never keeps a value as the truth, and never performs an action itself.

Ogen speaks MCP, so any MCP client connects with a URL and a key or a sign-in, and every tool is also a plain REST call. The fastest first result needs no code: the Google Sheet walkthrough below.

The docs, page by page

  • Connect a client: Claude Code, Cursor, stdio clients, frameworks, sign-in and plain HTTP.
  • Tool reference: eight tools your agent uses, and the operator tools behind them.
  • Sources: what a key can read, with a set-up walkthrough for each system.
  • Actions & holds: how a held action reaches the right person and gets decided.
  • Ogen Assist: an assistant that prepares each hold for a person, on your own model key.
  • Run your tenant: plans, keys and roles, activity, shadow and evidence mode, deletion.

Quick start with a Google Sheet (no code)

For an operations person with a Google Sheet and Claude Desktop or Cursor: no terminal, no install.

1. Get a free key

Use the signup form on the home page; it asks only for an email address. The key arrives by email and is shown once. Binding your own sheet (step 4) and gating an action on it (step 6) need Record. It starts free the first time you use it, and stays free until Ogen has held 10 actions (see Plans).

2. Make the sheet readable

Ogen reads Google Sheets through Google's own Sheets API, which always needs a key, even for a sheet you consider "public," and doesn't support per-person sharing (no OAuth, no service account). Two things, both required:

  1. Share the sheet by link. In the sheet, File → Share (or the Share button), then set general access to "Anyone with the link", role Viewer. Take "anyone with the link" literally: put nothing in this sheet you wouldn't show anyone who has the link.
  2. Get a Google Sheets API key. Follow Google's instructions for creating an API key, then restrict it to the Google Sheets API only. You paste it into the chat in step 4. Ogen stores it encrypted and never shows it again, so keep your own copy.

3. Point your client at Ogen

For Claude Desktop, paste this into claude_desktop_config.json, with your own key in place of ogen_sk_…:

{
  "mcpServers": {
    "ogen": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://us.ogenhq.com/api/mcp", "--header", "Authorization: Bearer ogen_sk_…"]
    }
  }
}

Cursor speaks Streamable HTTP directly: use the "type": "http" block from Connect a client instead, no bridge needed. Restart the client afterward so it picks up the new server.

4. In plain chat, ask it to bind the sheet

No JSON to write by hand. Describe the sheet in one sentence and let the assistant fill in the call:

Using Ogen, bind the key customer.address.acme to my Google Sheet.
The sheet ID is 1AbC2dEfGhIjKlMnOpQrStUvWxYz1234567890,
the range is Sheet1!A2:B200, names are in column A and
addresses are in column B, and my Sheets API key is AIzaSy…

The sheet ID is the long string in the sheet's URL, between /d/ and /edit: https://docs.google.com/spreadsheets/d/1AbC2dEfGhIjKlMnOpQrStUvWxYz1234567890/edit. Column indexes start at 0: column A is 0, column B is 1, and so on. Your assistant then calls bind_source:

{
  "key": "customer.address.acme",
  "source": {
    "kind": "gsheets",
    "sheet_id": "1AbC2dEfGhIjKlMnOpQrStUvWxYz1234567890",
    "range": "Sheet1!A2:B200",
    "key_column_index": 0,
    "value_column_index": 1,
    "api_key": "AIzaSy…"
  }
}

The key's last part is the row. A Google Sheet lookup matches the row whose name-column cell equals the key's last dot-separated part — so customer.address.acme looks up the row named acme, not the whole key. Name column A with your row names (acme, globex, and so on) and give every customer's key that same last part.

A successful bind comes back { "bound": true, "key": "customer.address.acme", "kind": "gsheets", "label": "1AbC2dEfGhIjKlMnOpQrStUvWxYz1234567890" }. A call missing a required field is refused on the spot, before anything is bound; Something not working? shows what that looks like.

5. Ask for the fact

In plain chat: "What's Acme's address right now?" Your assistant calls recall_live_fact with { "key": "customer.address.acme" }, and one of three things comes back:

  • One value. The sheet has exactly one row for Acme: you get that address, as of the moment you asked.
  • Several that disagree. Two rows for Acme with different addresses: you see both, side by side. Nobody picks one; a person settles it, usually by fixing the sheet.
  • Nothing there. No row matches, the sheet isn't shared right, or the key was never bound: you get a plain "unavailable," never an empty answer dressed up as a real one.

6. Gate an action on it

Say a "notify the customer" action should only go ahead once the address is settled, with a named person signing off. Register the action and its approver, in chat:

Register an action called notify-customer-address-change that
depends on customer.address.acme, cost reversible, approver Dana.

Register Dana as an approver: dana@example.com

That's one register_action call:

{ "kind": "notify-customer-address-change", "key": "customer.address.acme", "cost": "reversible", "approver": "Dana" }

and one register_approver call:

{ "name": "Dana", "email": "dana@example.com" }

Then ask it to run the action: "Notify the customer about their address." That's commit_gated_action with { "kind": "notify-customer-address-change" }. With one settled address in the sheet, it comes back drafted: prepared, waiting on Dana. With the sheet disagreeing with itself, it comes back escalated: it waits until the sheet settles, then goes the way a settled sheet would. Either way, nothing runs yet.

What Dana sees. One email: an action is waiting, it depends on customer.address.acme, that fact was read from your Google Sheet, and why it's waiting. Never the address or any other value; those stay behind the link. The confirm page names Dana as the approver and sends a one-time sign-in link to her own address, so the decision can only come from her inbox, not from anyone who has the confirmation link. She Confirms or Rejects, and only Confirm lets the action go ahead.

7. Change the sheet, ask again

Edit the cell in your sheet (fix the address, or make two rows agree) and ask "What's Acme's address now?" again. No waiting, no re-binding: every recall_live_fact call reads the sheet fresh, so the answer reflects the edit at once.

Something not working?

However a live read against a spreadsheet goes wrong, recall_live_fact answers the same way: { "status": "unavailable", "reason": "source_unreachable" }, never Google's own error text, which would also tell an outsider holding your key what to try next. So work through these in order:

  • Sheet not shared. Re-check File → Share: general access has to say "Anyone with the link," role Viewer.
  • Wrong column index. Indexes start at 0: column A is 0, not 1. An off-by-one quietly matches nothing.
  • Range too narrow. A range like Sheet1!A1:B1 covers only the header row. Widen it to every data row you want found.
  • API key restricted to the wrong API. Open the key in Google Cloud Console and confirm it's restricted to (or at least allows) the Google Sheets API.

One case is specific: a bind_source call missing a required field (say, no sheet_id) is refused on the spot, before anything is bound, naming the field:

bind_source refused: [{"code":"invalid_type","expected":"string","received":"undefined","path":["sheet_id"],"message":"Required"}]

The part that matters is "path":["sheet_id"]: the one field to add. Once bind_source succeeds, every cause above comes back as the same plain "unavailable."