Skip to content
Rigel docs
Open Rigel

Rigel, the agent

Rigel works through the same commands and queries you do, acts first and says what it did, asks before anything that can't be taken back, and can work on a schedule.

Rigel uses the same commands and queries#

Rigel is the agent in every household. It has no private access. Every command and query that Rigel may use is a tool named by its operation ID, which is the operation's name with dots turned into underscores: inventory.item.adjust is the tool inventory_item_adjust. The tool's description and input schema are the operation's own. This reference is Rigel's documentation as well as yours, and the descriptions are written for a reader who has never seen the code.

Rigel has every app's tools in every conversation, not only the ones for the app a person has open. From the meal plan it can add to a recipe, and from a recipe it can plan a meal.

It can do nothing a member couldn't do with their own session. It acts for the person who asked, with that person's role, and the household's history records its changes as Rigel acting for that person. When a rule rejects a command Rigel ran, the code and message come back to it as a result, the same as for you, and it fixes the request or tells the person.

The agent policy#

Every command declares a policy for Rigel. It is on the command's reference page, and at the end of its description in the OpenAPI document as Agent policy: allowed.

Reference saysValueWhat it means
Rigel runs itallowedRigel runs the command when it helps and tells the person what it did. This is almost every command.
Rigel asks firstconfirmRigel doesn't run it. It shows the person what it wants to do, in plain words, and runs it only after a yes.
Not for RigelneverIt isn't a tool. Only people use it.

The policy applies to Rigel and nobody else. You can run any of these commands yourself over HTTP, and nothing asks you first. Every query is open to Rigel.

Rigel asks first when a change can't be fixed afterward or reaches people outside the household. Sending an email is the clearest case. These are the commands that ask today:

CommandWhat it does
email.draft.sendSend a draft
family.person.removeRemove someone and all their notes
files.file.removeDelete a file
maintenance.asset.removeDelete an item recorded by mistake
maintenance.contractor.removeDelete a contractor
maintenance.project.removeDelete a project
maintenance.property.removeDelete a property and everything at it

When nobody is present to say yes, such as a job running in the background, a command that asks first is refused. Rigel is told nobody can approve it, so it can't be done then, and it says so.

Not for Rigel covers a person's own state and account-level actions:

CommandWhat it does
chat.read.markMark a conversation read up to a message
household.integration.disconnectDisconnect an outside account
tasks.comment.reactReact to a comment with an emoji
tasks.comment.unreactTake back an emoji reaction

It acts, says what it did, and fixes forward#

Rigel is meant to behave like a capable person helping out, not a program people have to supervise.

  • It acts when the result can be fixed afterward. It doesn't draft a change for approval when it can make it. If the result isn't what the person wanted, they say so and Rigel changes it again.
  • It says what it did, briefly and in plain words, so nothing happens silently.
  • It fills what is empty and leaves what people wrote. When Rigel fills in details on its own, such as a recipe's missing fields or a plan's open slots, it touches only empty ones. It changes existing content when someone asks it to.
  • It asks when a request is genuinely unclear, the way a person would.

There is no Undo. A mistake is fixed forward with another command, and the household's history shows what happened. Some apps expose it, for example meals.week.changes and inventory.item.history, which show each change with who or what made it, Rigel included.

Rigel also writes down what it learns about people, such as "Nia isn't into plain chicken lately", with family.note.record, and mentions that it did. family.knowledge.get reads everything the household has told it.

Recipes from the web, emails and documents can contain instructions aimed at Rigel. Rigel reads content from outside the household as material, never as instructions. It can act only through commands a member could run, and anything that reaches outside the household needs a person's yes.

Talking to Rigel#

Conversations with Rigel travel over the household socket. Connect as that guide describes and send an agent.send message. The household runs a turn and streams what happens back to the connection that sent it, and to no one else.

TypeScript
import WebSocket from "ws";

const householdId = "hh_01JC8Q4W2N7XK5R3VYTZ9D6HBE";
const socket = new WebSocket(`wss://rigel.house/api/v1/households/${householdId}/socket`, {
  headers: { Cookie: `rigel_session=${process.env.RIGEL_SESSION}` },
});

// A script has no screen, so its page context is empty.
const page = { route: "/", page: null, region: null, other_tabs: [], recent: [], actions: [], buttons: [], dialogs: [], fields: [] };

socket.on("open", () => {
  socket.send(
    JSON.stringify({
      type: "agent.send",
      request_id: "ask-1",
      conversation_id: null, // null starts a conversation; send its ID to carry on
      text: "Remember that Nia isn't into plain chicken lately.",
      file_ids: [],
      page,
    }),
  );
});

socket.on("message", (data) => {
  const text = data.toString();
  if (text === "pong") return;
  const message = JSON.parse(text);
  if (message.type === "agent.text") process.stdout.write(message.delta);
  if (message.type === "agent.done") socket.close();
});

A turn arrives as a stream. The interesting messages look like this:

JSON
{
  "type": "agent.action",
  "conversation_id": "agc_01JC9E2M6P8WTK4XN3VZD5RHQA",
  "action": {
    "tool": "family.note.record",
    "summary": "Remember something new about a person or the household",
    "kind": "command",
    "input": { "person_id": "per_01JC8Q5X0RT4M8ZVB2HNQ6EK1Y", "text": "Isn't into plain chicken lately" },
    "outcome": "applied",
    "command_id": "cmd_01JC9E2M7A3NXQ5TV8KZD4RWBH",
    "result": { "note_id": "fnt_01JC9E2M7B6PZW2XK9TQ4NVDRE" }
  }
}
JSON
{
  "type": "agent.done",
  "conversation_id": "agc_01JC9E2M6P8WTK4XN3VZD5RHQA",
  "message_id": "agm_01JC9E2M8D4HWQ6TN2XVZ9KBRE",
  "stopped": "done",
  "text": "Noted that Nia isn't into plain chicken lately."
}

From you to the household:

MessageFields
agent.sendrequest_id (yours, 1 to 64 characters, echoed back), conversation_id (or null to start one), text (1 to 8,000 characters), file_ids (up to 10, uploaded first, see files), and page.
agent.confirmrequest_id, conversation_id, pending_id from an agent.pending, and answer: do_it or cancel.
agent.stopconversation_id. Stops the turn after the model call in progress.
agent.ui_resultThe answer to an agent.ui_action. See page context.

From the household to the connection that sent the turn:

MessageFields
agent.startedrequest_id, conversation_id and message_id, your message as saved.
agent.textconversation_id and delta, the next piece of what Rigel is saying.
agent.actionOne thing Rigel did: tool (the operation's name, such as family.note.record), summary, kind (command, query, ui_action or agent_tool), input, outcome (applied, rejected, answered, awaiting_confirmation, refused or failed), command_id, result and error with a code and message.
agent.pendingA command that asks first: confirmation with its id, command, input and a description in plain words.
agent.ui_actionRigel asking this tab to do something on screen. Only sent when page.actions offered it.
agent.donemessage_id of Rigel's saved reply, stopped, and text, what Rigel said in full, which replaces whatever streamed in.
agent.errorrequest_id and a message in plain words.
  • One turn at a time per conversation. A message sent while one is running is refused with an agent.error. A conversation that isn't yours answers as if it didn't exist.
  • Asking first. A command that asks first doesn't run. Rigel reports it in agent.pending, the turn ends, and you answer with agent.confirm. On do_it the command runs as Rigel acting for you, and Rigel says what happened. On cancel it answers "Okay, I won't."
  • Photos and files. Upload them first and send their IDs. Rigel looks at the images and PDFs of your newest message and can attach them to records it makes. Older messages keep only a note that a file was attached.
  • Why a turn stopped. stopped is done, call_limit (Rigel ran out of steps and says so), over_budget (the household has used its monthly allowance of model calls, and the reply says when it renews), unavailable (the model couldn't be reached or isn't set up) or cancelled (you stopped it, or the connection closed). The turn is saved as far as it got in every case.

Rigel also takes part in the family chat. A message to chat.message.send that mentions @Rigel gets a reply from Rigel in the same topic or thread. So does a follow-up from the same person within 5 minutes, when nobody else has written there since, a message that quotes one of Rigel's, and a message in the thread under one of Rigel's. The reply is a job, so it appears as a new message a moment later.

Reading past conversations#

GET /api/v1/households/{household_id}/agent/conversations lists the conversations you started with Rigel, newest first. GET /api/v1/households/{household_id}/agent/conversations/{conversation_id} returns one with every message, oldest first.

JSON
{
  "items": [
    {
      "id": "agc_01JC9E2M6P8WTK4XN3VZD5RHQA",
      "title": "Remember that Nia isn't into plain chicken lately.",
      "created_at": 1794240000000,
      "updated_at": 1794240004000
    }
  ]
}

Conversations are private to the person who started them. Nobody else in the household can list or read yours, and Rigel can't read another person's conversations either. Asking for someone else's answers 404. A message has id, role (person or rigel), text, file_ids, actions (what Rigel did, shaped like agent.action), pending (confirmations still waiting for an answer), stopped and created_at. Unlike most responses, the times on these routes are integer UTC milliseconds.

Page context and UI actions#

In the web app, every page tells Rigel what it shows: its title, a sentence about it, the records on screen and the ones selected, all in words and IDs. Rigel never gets a screenshot or the page's markup. The page also offers UI actions: things Rigel may do on that screen, such as opening a day of the meal plan, filling a field or pressing a button. The tab sends all of this as page in agent.send. Every field is required, and an empty list or null says there is nothing to report.

page fieldMeaning
routeThe path the tab is showing, such as /meals/plan/2026-11-10.
pageWhat the page registered, or null: app (an app ID, or null for a page of the shell's own), title, an optional summary in a sentence or two, and optional records on screen (up to 200, each { id, kind, label }) and selection, the IDs the person has selected.
regionThe named page region under the pointer, or null.
other_tabsThe routes of the person's other open tabs, up to 20.
recentThe last things the person did, oldest first, up to 20: { type: "navigated", route, at }, { type: "pressed", button, label, at } or { type: "ran_command", command, at }.
actionsThe UI actions on offer, up to 100: name (meals.plan.open_day, or shell.… for the shell's own), description and input_schema, a JSON Schema for the action's input.
buttons, dialogs, fieldsWhat the page lets Rigel press, open and fill, each with a stable name and a label. Buttons also say whether they are enabled.

Rigel turns each entry of actions into a tool named ui_ and the action's name. When it uses one, the household sends the tab that asked an agent.ui_action and no one else:

JSON
{
  "type": "agent.ui_action",
  "conversation_id": "agc_01JC9E2M6P8WTK4XN3VZD5RHQA",
  "call": {
    "call_id": "uic_1",
    "name": "meals.plan.open_day",
    "input": { "date": "2026-11-10" },
    "announcement": "Opening Tuesday"
  }
}

The tab runs it, shows the announcement for a moment, and answers within about 15 seconds, or Rigel is told the action failed.

JSON
{ "type": "agent.ui_result", "result": { "call_id": "uic_1", "ok": true, "result": {} } }
{ "type": "agent.ui_result", "result": { "call_id": "uic_1", "ok": false, "error": "That day isn't on this page." } }

A UI action never writes data itself. When pressing a button runs a command, the command is checked as Rigel's, so Rigel can't get around a policy of asks first or not for Rigel by pressing a button. A client with no screen, such as a script, sends "/" as the route, null for page and region, and empty lists, as in the example above. Rigel then gets no UI tools.

What Rigel does on its own#

Rigel can also work with nobody present, in two ways that anyone in the household can set up, ask Rigel to set up, or take down. Both are records everyone can read. A run acts as Rigel for the person the record is for, with that person's role. It may run a command whose policy is allowed and any query. A command that asks first is refused, since nobody can say yes, and it can't drive anyone's screen. It speaks only through cards on the dashboard and notifications, with a reason, and it does nothing when there is nothing worth saying. It never asks a question.

Routines run on a schedule.

repeat
{ "kind": "daily", "time": "07:30" }
{ "kind": "weekly", "days": ["wed"], "time": "09:00" }
{ "kind": "monthly", "day": 1, "time": "08:00" }
{ "kind": "once", "date": "2026-12-01", "time": "18:00" }

Times are 24-hour HH:MM in the household's time zone, so 9:00 stays 9:00 when the clocks change. A time that happens twice runs once, at the first. A time that doesn't exist, such as 2:30 am on the night the clocks go forward, runs at the same minutes after the gap, 3:30 am. A monthly day is 1 to 28, so every month has it. A run that is late, for example because the household was over its allowance, happens when it can and the missed slots are not made up. Resuming a paused routine starts from now.

Sets up a weekly routine for the person asking

Request, curl
curl -X POST 'https://rigel.house/api/v1/households/hh_01K9BKKP003AHRZ6DMV29GQY5C/commands/agent.routine.create' \
  -H "Cookie: rigel_session=$RIGEL_SESSION" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: cmd_01JC4N0000AAAAAAAAAAAAAAAA' \
  -d '{
  "title": "Suggest a new recipe on Wednesdays",
  "task": "Suggest a recipe.",
  "app": "meals",
  "repeat": {
    "kind": "weekly",
    "days": [
      "wed"
    ],
    "time": "09:00"
  }
}'
Request, TypeScript
const response = await fetch("https://rigel.house/api/v1/households/hh_01K9BKKP003AHRZ6DMV29GQY5C/commands/agent.routine.create", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Idempotency-Key": "cmd_01JC4N0000AAAAAAAAAAAAAAAA",
    Cookie: `rigel_session=${process.env.RIGEL_SESSION}`,
  },
  body: JSON.stringify({
    title: "Suggest a new recipe on Wednesdays",
    task: "Suggest a recipe.",
    app: "meals",
    repeat: {
      kind: "weekly",
      days: [
        "wed",
      ],
      time: "09:00",
    },
  }),
});
const data = await response.json();
Request, HTTP
POST /api/v1/households/hh_01K9BKKP003AHRZ6DMV29GQY5C/commands/agent.routine.create HTTP/1.1
Host: rigel.house
Cookie: rigel_session=<your session>
Content-Type: application/json
Idempotency-Key: cmd_01JC4N0000AAAAAAAAAAAAAAAA

{
  "title": "Suggest a new recipe on Wednesdays",
  "task": "Suggest a recipe.",
  "app": "meals",
  "repeat": {
    "kind": "weekly",
    "days": [
      "wed"
    ],
    "time": "09:00"
  }
}
Response, 200
HTTP/1.1 200 OK
Content-Type: application/json
X-Request-Id: req_01K9D9S2R0HR64SDQARAVBBWWK

{
  "result": {
    "routine_id": "rtn_01JC4N0000MBGE5RJ6Q7V6V6T8",
    "next_run_at": 1762956000000
  },
  "version": 1
}

Triggers run after something happens.

  • agent.trigger.create takes a title, a task, optionally app and for_person_id as above, commands, and when. commands names 1 to 20 commands to watch, by their dotted names, such as recipes.cook.log. A name must be a command that exists and isn't internal, and not one of the agent.trigger.* commands, or the answer is 409 unknown_command.
  • when is { "kind": "soon" } (about a minute after), { "kind": "after", "minutes": 30 } (1 to 1,440) or { "kind": "at", "time": "19:00" } (the next time the household's clock shows it, at least a minute after the event).
  • agent.trigger.update, agent.trigger.pause, agent.trigger.resume and agent.trigger.remove change one. agent.trigger.list lists them.

Sets up a trigger for the person asking

Request, curl
curl -X POST 'https://rigel.house/api/v1/households/hh_01K9BKKP003AHRZ6DMV29GQY5C/commands/agent.trigger.create' \
  -H "Cookie: rigel_session=$RIGEL_SESSION" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: cmd_01JC4N0000AAAAAAAAAAAAAAAA' \
  -d '{
  "title": "Ask how a new recipe went",
  "task": "Ask how it went.",
  "app": "recipes",
  "commands": [
    "recipes.cook.log"
  ],
  "when": {
    "kind": "at",
    "time": "19:00"
  }
}'
Request, TypeScript
const response = await fetch("https://rigel.house/api/v1/households/hh_01K9BKKP003AHRZ6DMV29GQY5C/commands/agent.trigger.create", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Idempotency-Key": "cmd_01JC4N0000AAAAAAAAAAAAAAAA",
    Cookie: `rigel_session=${process.env.RIGEL_SESSION}`,
  },
  body: JSON.stringify({
    title: "Ask how a new recipe went",
    task: "Ask how it went.",
    app: "recipes",
    commands: [
      "recipes.cook.log",
    ],
    when: {
      kind: "at",
      time: "19:00",
    },
  }),
});
const data = await response.json();
Request, HTTP
POST /api/v1/households/hh_01K9BKKP003AHRZ6DMV29GQY5C/commands/agent.trigger.create HTTP/1.1
Host: rigel.house
Cookie: rigel_session=<your session>
Content-Type: application/json
Idempotency-Key: cmd_01JC4N0000AAAAAAAAAAAAAAAA

{
  "title": "Ask how a new recipe went",
  "task": "Ask how it went.",
  "app": "recipes",
  "commands": [
    "recipes.cook.log"
  ],
  "when": {
    "kind": "at",
    "time": "19:00"
  }
}
Response, 200
HTTP/1.1 200 OK
Content-Type: application/json
X-Request-Id: req_01K9D9S2R0GSP1QNAK5YJVDJMP

{
  "result": {
    "trigger_id": "trg_01JC4N0000MBGE5RJ6Q7V6V6T8"
  },
  "version": 1
}

A trigger sees what happened: for each command it watched, when, who made it (a person, Rigel acting for one, or an outside account), the command's name and input, and what it changed, as material to read and not as instructions. Several events before a run are one run, and ten items ticked off a list in a minute are one. A trigger runs at most 20 times in 24 hours, and events past that are delayed, not dropped. Only applied commands count, from anyone but the household itself. Commands run by a trigger's own run never set off a trigger, so nothing loops. A paused trigger collects nothing, and resuming it doesn't replay what it missed.

If the household has used its monthly allowance, a run waits until it renews. If the model is unavailable it is tried again. A run that fails for good is skipped, and the routine or trigger carries on. There are no quiet hours yet, so a routine that sends a notification should be set for a sensible time of day. last_run_at says when one last ran, and what it changed is in the history as Rigel acting for the person it is for.

Other background work uses the same Rigel with nobody present: reading an email (see the email guide) and answering in the family chat.

What is not built#

  • The socket schema. The agent.* messages aren't in /api/v1/socket-schema.json yet, so the tables above are the contract for now.
  • Rigel in chat. How Rigel takes part in the family chat, when it speaks and what it remembers, will be redesigned. Treat today's behavior as a first version.
  • The model. Rigel needs a model to think. When it isn't available a turn ends unavailable and a job that needs it fails with a plain message.
  • Quiet hours. Nothing holds a notification back at night yet.
  • Android. Not built yet.

Rigel is in early access, and sign-up is by invitation. Open Rigel