Skip to content
OUTIS DOCS

Execution

A request can carry a sealed intent: the call your code wants to make, encrypted under a key only you hold. Outis stores the ciphertext and hands it back. It can’t read it, and it can’t change it.

Once the request is authorized, a worker you run finds the intent, claims it, decrypts it, checks it against what the operators approved, runs it with your own credentials and reports back. Outis keeps the claim ledger and fires an event when the report lands. It runs nothing.

The three routes here need a key holding execute. A worker’s key holds read and execute.

The plaintext is UTF-8 JSON, written once by the proposer and never re-serialized:

{"v":1,"client":"stripe","method":"transfers.create","args":[{"amount":9900,"currency":"usd","destination":"acct_9f2"}]}

client is the name your worker registered a client under, method is a dotted path on it, and args are the positional arguments. The proposer puts "sha256:" + hex(SHA-256(plaintext)) in the request’s params under intent, so the operation hash covers it and the operators approve that exact call.

The key is 32 random bytes, base64. Its id is the first 16 hex characters of SHA-256 over the key bytes, so a worker holding several keys during a rotation picks the right one. The plaintext is sealed with AES-256-GCM under a fresh 12 byte nonce, and the additional data is outis.intent.v1, a zero byte, then the request’s action. An envelope sealed for one action won’t open under another.

The envelope goes on POST /v1/requests as intent, beside params.intent:

FIELDWHAT IT IS
vThe envelope version.
algThe one algorithm.
kidThe first 16 hex characters of SHA-256 over the key, so a worker holding several keys picks the right one.
nonce12 random bytes, base64url.
ciphertextThe encrypted plaintext with the 16 byte tag appended, base64url.

execute_within on the same call sets how long after the verdict a worker may still claim it, in seconds. It defaults to seven days and tops out at thirty.

Every request object carries intent, the envelope verbatim or null, and execution:

"execution": {
  "state":            "claimed",
  "claimed_at":       <epoch ms>,
  "lease_expires_at": <epoch ms>,
  "reported_at":      null,
  "reference":        null,
  "error":            null,
  "execute_by":       <epoch ms>
}
FIELDWHAT IT IS
stateWhere the intent stands, read at the server's clock. A lease that ran out reads `pending` again.
claimed_atWhen the current claim was made.
lease_expires_atWhen the current claim stops being exclusive.
reported_atWhen the claim holder reported.
referenceWhat the worker said the run produced, like a transfer id.
errorWhat the worker said went wrong.
execute_byThe last instant a worker may claim the intent, the verdict plus `execute_within`.
STATEMEANS
noneNo intent, or one that can never run because the request wasn't authorized.
pendingAuthorized and waiting for a worker to claim it.
claimedA worker holds a live lease on it.
succeededThe claim holder reported that the run worked.
failedThe claim holder reported that the run failed.

execution never moves the request’s own state. An action your organization defined goes from authorized to succeeded when the keys turn, whatever your worker does afterwards; execution.state is where your run stands.

GET /v1/requests?executable=true&limit=50

Your organization’s requests that are authorized, carry an intent, haven’t been reported, aren’t under a live claim and are still inside their window. Oldest first, at most 100. A worker polls this when it doesn’t take webhooks, or to catch one that never arrived.

curl "https://api.outis.tech/v1/requests?executable=true" \
  -H "Authorization: Bearer $OUTIS_WORKER_KEY"
{
  "server_now": <epoch ms>,
  "requests": [ { ...a request object, execution.state pending... } ]
}
STATUSWHEN
400`executable` isn't `true`, or `limit` is out of range. Kind `invalid`.
401No live key. The body names neither the key nor the reason.
403The key is live and doesn't hold the execute scope.
429Too many refused attempts from this peer. Carries `Retry-After`.
500The requests couldn't be read. Kind `internal`.
POST /v1/requests/{id}/claim

Hands the intent to one worker until the lease runs out. However many workers claim at once, on however many Outis instances, exactly one gets a 200. The rest get a 409 with kind already_claimed.

Optional. An empty body takes the default lease.

FIELDWHAT IT IS
lease_secondsoptionalHow long the claim is yours alone. Absent is 600.
curl -X POST "https://api.outis.tech/v1/requests/req-4f2a9c1b8d7e6f50/claim" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $OUTIS_WORKER_KEY" \
  -d '{"lease_seconds": 300}'
FIELDWHAT IT IS
server_nowThe server's clock when it answered, so a worker can time its lease against it.
claim_idSend it back with the report. clm_ and 24 hex characters.
lease_expires_atWhen the lease runs out, UTC epoch milliseconds.
requestThe request object, intent included, as of the claim.

A lease that runs out without a report is claimable again, and the new claim replaces the old one. That’s how a worker that crashed mid run gets covered. It’s also why the downstream call has to carry the request id as its idempotency key: the second worker may be repeating a call the first one already made.

STATUSWHEN
400The body is unreadable or `lease_seconds` is out of range. Kind `invalid`.
401No live key. The body names neither the key nor the reason.
403The key is live and doesn't hold the execute scope.
404No such request in your organization. Kind `not_found`.
409Nothing to claim: another worker holds a live lease (`already_claimed`), the run was already reported (`already_reported`), the request isn't authorized (`not_authorized`), or it carries no intent (`no_intent`).
410The execution window closed: `execute_by` has passed. Kind `execution_window_closed`. Propose it again if it still needs doing.
413The body is larger than 4 KiB. Kind `too_large`.
415A body was sent and it isn't application/json. Kind `unsupported_media_type`.
429Too many refused attempts from this peer. Carries `Retry-After`.
500The request couldn't be read or saved. Kind `internal`. Retry the claim.
POST /v1/requests/{id}/execution

The claim holder says how the run went. Only the current claim can report. A report after the lease ran out still counts if nobody claimed since. The same report twice is a 200 both times and fires one event.

FIELDWHAT IT IS
claim_idrequiredThe claim the run was made under.
statusrequiredHow the run went.
referenceoptionalWhat the run produced, like the downstream object's id.
erroroptionalWhat went wrong, for the people reading the request.
curl -X POST "https://api.outis.tech/v1/requests/req-4f2a9c1b8d7e6f50/execution" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $OUTIS_WORKER_KEY" \
  -d '{"claim_id": "clm_5b0e2f7c91d34a86e1f0c2d3", "status": "succeeded", "reference": "tr_1Q2w3E"}'

The request object wrapped with server_now, the same as GET /v1/requests/{id}. Outis then posts request.executed or request.execution_failed to the request’s callback URL and your webhook endpoints. See Events and callbacks.

STATUSWHEN
400The body is unreadable, has no `claim_id`, has a `status` other than `succeeded` or `failed`, or a `reference` or `error` over 512 characters. Kind `invalid`.
401No live key. The body names neither the key nor the reason.
403The key is live and doesn't hold the execute scope.
404No such request in your organization. Kind `not_found`.
409Someone claimed after you (`claim_lost`), a different report is already recorded (`already_reported`), or the request carries no intent (`no_intent`).
413The body is larger than 4 KiB. Kind `too_large`.
415This route reads application/json. Kind `unsupported_media_type`.
429Too many refused attempts from this peer. Carries `Retry-After`.
500The request couldn't be read or saved. Kind `internal`. Send the same report again.

The worker checks, in this order, and reports failed without running anything if one doesn’t hold:

  1. It holds a key whose id matches the envelope’s kid.
  2. The envelope decrypts with the additional data built from the request’s action.
  3. SHA-256 of the plaintext matches params.intent.
  4. operation_hash matches the operation hash it computes from action and params.
  5. outcome is authorized.
  6. The intent’s client is one the worker registered, and the method is one it allows.

Outis can’t forge a call that passes step 3, because it doesn’t hold your key. A proposer can write any intent it likes, and people still approve its exact digest before anything runs.

KINDMEANS
invalidThe body or a query parameter is malformed. A 400.
not_foundNo such request in your organization. A 404.
no_intentThe request carries no intent. A 409.
not_authorizedThe request isn't authorized, or its authorization no longer stands. A 409.
already_claimedAnother worker holds a live lease. A 409; try again after it runs out.
already_reportedThe run was already reported. A 409.
claim_lostSomeone claimed after you, so your report isn't the current one. A 409.
execution_window_closed`execute_by` has passed. A 410.
too_largeThe body is over 4 KiB. A 413.
unsupported_media_typeThe body isn't application/json. A 415.
internalSomething failed on the server's side. A 500; retry.

A key that’s absent, revoked or lacks execute gets a 401 or 403 from the key check, with error alone.