Skip to content
Rigel docs
Open Rigel

Commands

A command is the only way to change a household: one named action with a JSON input, run once and recorded with who made it.

Naming, request and response#

A command is named <app>.<record>.<verb>, in lowercase with underscores inside each part: family.note.record, inventory.item.adjust, meals.meal.plan. The name is the route, and the OpenAPI operationId is the same name with the dots replaced by underscores (inventory_item_adjust). A name is never reused for different behavior.

Text
POST /api/v1/households/{household_id}/commands/{name}

Send a JSON object as the body, with Content-Type: application/json, even when every field is optional. Field names are snake_case. The body is the command's input, and its fields are listed on the command's reference page. A request that doesn't fit the input is refused with 400 validation_failed before the command runs.

The first person goes first, with an ID derived from the command

Request, curl
curl -X POST 'https://rigel.house/api/v1/households/hh_01K9BKKP003AHRZ6DMV29GQY5C/commands/family.person.add' \
  -H "Cookie: rigel_session=$RIGEL_SESSION" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: cmd_01JC4N0000AAAAAAAAAAAAAAAA' \
  -d '{
  "name": "Nia"
}'
Request, TypeScript
const response = await fetch("https://rigel.house/api/v1/households/hh_01K9BKKP003AHRZ6DMV29GQY5C/commands/family.person.add", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Idempotency-Key": "cmd_01JC4N0000AAAAAAAAAAAAAAAA",
    Cookie: `rigel_session=${process.env.RIGEL_SESSION}`,
  },
  body: JSON.stringify({
    name: "Nia",
  }),
});
const data = await response.json();
Request, HTTP
POST /api/v1/households/hh_01K9BKKP003AHRZ6DMV29GQY5C/commands/family.person.add HTTP/1.1
Host: rigel.house
Cookie: rigel_session=<your session>
Content-Type: application/json
Idempotency-Key: cmd_01JC4N0000AAAAAAAAAAAAAAAA

{
  "name": "Nia"
}
Response, 200
HTTP/1.1 200 OK
Content-Type: application/json
X-Request-Id: req_01K9D9S2R0N24QBC0MH6021006

{
  "result": {
    "person_id": "per_01JC4N0000MBGE5RJ6Q7V6V6T8"
  },
  "version": 1
}

A command that was applied answers 200 with two fields.

  • result is what the command returns, such as the ID of the person it created. It is {} when a command has nothing to return.
  • version is the household's version after the command ran. The version is a number that goes up by one for every applied command. A rejected command doesn't change it. A client that keeps a local copy uses it to know what to pull (see the sync guide).

A command is applied when it answers, but work it starts afterward may not be finished. Drawing a recipe's picture, reading a web page and sending an email run later as jobs, and their results arrive as ordinary changes to the records.

Send only fields the command defines. Today the API ignores a field it doesn't recognize instead of refusing it, so a misspelled optional field does nothing and says nothing. Check names against the reference.

Idempotency keys and retries#

The Idempotency-Key header makes a command safe to send again. The key must be a command ID: cmd_ and 26 characters from 0-9 and A-Z without I, L, O and U, such as cmd_01JC4N9B2K5T8W1X4Z7C0D3F6H. A ULID is the right choice, because its first 10 characters are the time, and the IDs the command creates are derived from the key and carry that time. The API checks the format, not the clock. The key becomes the command's ID in the household's log. See IDs and formats.

TypeScript
const alphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";

function commandId(): string {
  let time = Date.now();
  let id = "";
  for (let i = 0; i < 10; i++) {
    id = alphabet.charAt(time % 32) + id;
    time = Math.floor(time / 32);
  }
  for (const byte of crypto.getRandomValues(new Uint8Array(16))) id += alphabet.charAt(byte % 32);
  return `cmd_${id}`;
}
You sendYou get
A new keyThe command runs.
A key you used before, with the same command name and the same inputThe stored outcome, without running again: the same result and the same version as the first time. The order of fields in the body doesn't matter.
A key you used before, with a different command or input409 idempotency_key_reused. Nothing changes.
No keyThe command runs under a key the server makes up, so sending it again runs it again.

Always send a key. Retrying a create returns the same new ID, and retrying an inventory.item.adjust doesn't use up a second item. Keys are kept as long as the household's log, so they don't expire. Never reuse one for a different command.

A rejected command is stored too. Sending it again with the same key returns the same rejection, with details empty. After you change the household's state or fix the request, use a new key. A request refused with 400 validation_failed never reaches the household, so its key is still free to use.

Rejections#

A command that is valid but can't be applied to the household's current state is rejected. Nothing changes. The response is the standard error envelope (see Errors and retries) with a code that belongs to the command.

StatusWhenExamples
400The request doesn't fit the command's input. The command never ran.validation_failed
403Your role can't run the command.forbidden_role
404The code ends in _not_found: a record the command names isn't there.person_not_found, item_not_found
409Any other rejection: the household's state doesn't allow it.person_has_account, location_not_empty, name_taken

Each command's reference page lists the codes it can return. A message ends with "No changes were applied." details carries context when there is some, such as the ID that wasn't found. recovery says what to do next, usually to read the current state and send a command that fits it. A rejection depends only on the input and the household's data at that moment, so the same command can succeed later.

Commands say what the person meant#

Commands describe what someone did, not the state they want. To use up an item, send inventory.item.adjust with a delta of -1. Do not read the count, subtract one and send the new count. If two people use up the same item at once, both changes count.

Request body
{ "item_id": "inv_01JC4M9X4A1C3E5G7J9M2P4R6T", "delta": -1 }

Where the person did mean a total, there is a command for it. inventory.item.set_amount is for "we counted, and there are 3".

A command that edits fields, such as family.person.update, changes only the fields you send. Leave a field out to keep it. Send null, where the field allows it, to clear it.

Offline behavior and what Rigel may run#

Applies offline or waits for the server. Each command's page says which. A command that applies offline is run by the apps against their local copy at once and sent later. A command that waits for the server needs data only the server has, so an app queues it and shows it as pending until the server answers. Over HTTP the two behave the same: you always wait for the server's answer.

Rigel's policy. Each command also says how Rigel may use it.

LabelWhat it means for Rigel
Rigel runs itRigel may run it on its own, then says what it did. This is almost every command.
Rigel asks firstRigel shows the person what it will do and waits for a yes. It is used for what can't be fixed afterward or reaches outside the household, such as sending an email or removing a person and their notes.
Not for RigelRigel never runs it. This is for account-level actions.

The audit trail#

The household keeps a log of every command it received, applied or rejected, with when it ran and who or what ran it. It is the household's audit trail. The actor is one of these:

  • a person who signed in, in the app or with a session;
  • Rigel, acting for a person;
  • a personal API token, once tokens exist;
  • an outside account a person connected, such as Google Calendar, or mail arriving at Rigel's address;
  • the household itself, while it is being set up.

There is no undo. A mistake is fixed by sending another command, and the trail shows both. A script that uses your session is recorded as you. Text that people edit together over the socket is not recorded keystroke by keystroke. The household records it as one change, by the person who edited first, when the typing pauses. There is no route for reading the whole log yet. Some queries return the part that concerns their records, such as inventory.item.history and meals.week.changes, which say what changed and who or what changed it.

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