Skip to content

Reflex

Flows that run on their own: triggers, a catalogue of steps, a human approval before anything leaves the building, and an API to build, start and follow them.

Synapse routes, Cortex knows, Reflex acts. A flow is a small graph of steps that runs without anyone asking: a night ingestion, a 7 am sweep of deadlines, a review every Friday, three follow-up emails prepared for you to approve. Each run is recorded step by step, with what it read, what it concluded and what it did.

Three rules are enforced by the engine rather than suggested to the model:

  • Nothing leaves without a human. A step that sends something out of the building needs an approval in front of it.
  • A missed slot fires once. A machine that was off for three days runs the flow once when it wakes up, not once per period skipped.
  • Silence is a result. A run whose steps found nothing worth saying notifies nobody.

Triggers

Trigger nodeStarts the flow whenNotes
scheduleThe time expression is due.Grammar below. At least 5 minutes between runs.
manualSomeone presses Run, or POST /engine/flows/:id/run is called.Never fires by itself.
webhookAn outside system posts to the flow URL.Needs a shared secret. See below.
folder.changedA document arrives or changes in a watched folder.The folder is configured in the Cortex synchronisation settings.
mail.receivedA message arrives in a connected mailbox.Optional filter on the sender address or domain.

Schedule grammar. No cron strings: a flow nobody can read at a glance is one nobody dares change.

Expression Meaning
daily@08:30 Every day at 08:30 in the flow's time zone (default Europe/Paris).
weekly@fri@17:00 Every Friday at 17:00. Days: mon tue wed thu fri sat sun (French abbreviations also accepted).
every@30m, every@2h Every N minutes or hours. Minimum 5 minutes.
once@2026-11-03T09:00 One time, then the trigger switches itself off.

A schedule has active hours (windowHours, default 7,21): outside them the run waits instead of firing. And a misfire policy: fire-once (default) catches up once, skip forgets what was missed.

Catch-up and idempotency

Every scheduled run carries an idempotency key made of the flow and the slot it was scheduled for, and the key is unique. A Subsidia installation that wakes up after three days offline computes the slots it missed and the uniqueness collapses them into one run instead of 144. A one-shot (once@...) that was missed is still fired when the machine is back, because losing it is worse than running it late.

The flow's concurrency decides what happens when a trigger fires while a run is in progress: skip (default) drops the new one, queue waits for the previous, parallel runs both. Runs beyond the global ceiling wait in line; they are never dropped.

Steps (node catalogue)

Each step has a key (stable) and a type. A step's effect class decides how the engine treats it: read has no side effect, local-write stays on the machine, outbound leaves the building. The catalogue is data: GET /engine/node-types returns the full list with each step's configuration fields, which is what the editor itself uses.

TypeCategoryEffectWhat it does
scheduletriggerreadStarts on a time expression.
manualtriggerreadStarts when someone presses Run.
webhooktriggerreadStarts when an outside system posts to the flow URL.
folder.changedtriggerreadStarts when a document lands in a watched folder.
mail.receivedtriggerreadStarts when mail arrives in a connected mailbox.
agent.runagentreadOne full agent turn: its documents, memory and (optionally) tools. A silence word (default RAS) means nothing to report.
agent.debateagentreadThe workspace panel argues a question and converges on an answer.
cortex.searchcortexreadPassages that answer a question, with their sources.
cortex.readcortexreadThe text of one document, by id.
cortex.inventorycortexreadThe pieces a dossier contains, as a plain list with no model. It is what lets a flow say what is missing.
cortex.ingestcortexlocal-writeFiles a text from a previous step (or the trigger document) in a Cortex dossier. Same content means same piece: nothing is duplicated.
cortex.deadlinescortexreadReads the date columns of your registers (CSV, XLSX) and returns the approaching deadlines, exact rows, no model.
light.askcortexreadAsks Light: answers strictly from the documents with citations, or an honest refusal.
discovery.nightcortexreadRe-reads dossiers under watch overnight and prepares the morning questions: missing pieces, overdue deadlines, contradictions.
gatelogicreadStops the branch when the previous step had nothing worth saying (modes actionable, covered, uncovered, always).
branchlogicreadSends the flow one way or the other; the conditions are on the links.
delaylogicreadWaits 1 to 240 minutes.
notifyeffectlocal-writeTells you, through a configured channel or the in-app inbox.
file.writeeffectlocal-writeSaves a result to a folder on the machine.
mail.drafteffectlocal-writeWrites an email and stops. Nothing is sent.
http.calleffectoutboundCalls an external URL. A GET reads, anything else writes.
connection.calleffectoutboundCalls a service registered under Connections, with its stored credentials.
mail.sendeffectoutboundSends an email.
mail.replyeffectoutboundReplies to the sender of the mail that triggered the flow. Self-approving: the run always pauses on the exact message before anything is sent.
ask.humanhumanreadAsks you a question with context. Your answer is filed in the dossier, then the run resumes.
approvalhumanreadParks the run in your inbox until you approve or reject.

Passing a result to the next step. Any text field of a step can reference an earlier step by its key: {{http_call.output.status}}, {{agent_run.summary}}, {{trigger.output}}, plus {{date}}, {{time}}, {{flow}}. The reference uses the step key, which never changes when the label does. An unknown reference is left in the text as written, so a mistake is visible instead of silently blank.

Every step returns output, a one-line summary and actionable. The last one is the silence rule made structural: a gate stops the branch when it is false, and a run whose final steps are all non-actionable notifies nobody (for flows set to only-if-actionable).

The approval rule for outbound steps

Anything that stays on the machine runs unattended. Anything that leaves (mail.send, http.call, connection.call, mail.reply) is prepared, proposed and waits for a person.

The graph validator enforces it: an outbound step with no approval step directly feeding it is an unguarded-outbound error, and a flow with a blocking error cannot be switched on or run. Two things relax it, and only these two:

  • the step is flagged requiresApproval in its own configuration, which pauses the run on it;
  • the flow has allowUnattendedOutbound: true, an explicit waiver that is off by default and written to the access journal every time it changes.

mail.reply is self-approving: it always pauses with the exact message, even with no approval step in the graph and even with the waiver on.

Authentication and scope

Reflex lives under /engine/*. Developer API keys reach it with the reflex scope (keys created before scopes existed also work). Send Authorization: Bearer sk_live_... or x-api-key: sk_live_.... Every flow, run and approval is scoped to the workspace of the key. A read-only key can list and read, not create, change or run.

Endpoints

GET/engine/reflex/status

Is the scheduler running, and how many flows are there.

API key or sessionreflex

Request examples

curl "https://api.subsidia.protypa.fr/engine/reflex/status" \n -H "Authorization: Bearer $SUBSIDIA_API_KEY"

Responses

enabled is false on a cloud instance or when Reflex has been halted. The scheduler snapshot fields are added alongside.

Example response
{
"enabled": true,
"flows": 6,
"active": 4,
"pendingApprovals": 2
}

GET/engine/node-types

The step catalogue, with each step configuration fields.

API key or sessionreflex

The same definitions the validator, the executor and the editor read. Use it to build a valid config for a step, or to render your own form. minIntervalMinutes is the floor of recurring schedules.

Request examples

curl "https://api.subsidia.protypa.fr/engine/node-types" \n -H "Authorization: Bearer $SUBSIDIA_API_KEY"

Responses

The catalogue.

Example response
{
"nodeTypes": [
{
"type": "schedule",
"category": "trigger",
"effect": "read",
"label": "On a schedule",
"isTrigger": true,
"fields": [{ "key": "expr", "type": "schedule", "required": true, "default": "daily@08:30" }]
}
],
"minIntervalMinutes": 5
}

GET/engine/flows

List the flows of the workspace.

API key or sessionreflex

Request examples

curl "https://api.subsidia.protypa.fr/engine/flows" \n -H "Authorization: Bearer $SUBSIDIA_API_KEY"

Responses

Most recently updated first. schedule is a readable label of the schedule trigger.

Example response
{
"flows": [
{
"id": "flw_7a11",
"name": "Echeances de la semaine",
"description": "",
"enabled": true,
"nodeCount": 4,
"schedule": "Tous les jours a 08:30",
"nextRunAt": "2026-10-10T06:30:00.000Z",
"lastRun": { "id": "run_5d20", "status": "succeeded", "createdAt": "2026-10-09T06:30:01.000Z", "actionable": true },
"updatedAt": "2026-10-08T17:02:11.000Z"
}
]
}

POST/engine/flows

Create a flow.

API key or sessionreflex

Creates a flow with two starter steps (a daily 08:30 schedule feeding a notify), disabled. Replace the graph with PUT /engine/flows/:id/graph, then enable it.

Request body

  • namestringrequired
    Display name.
  • descriptionstring
    Free text.
  • timezonestringdefault Europe/Paris
    IANA time zone used to read schedule expressions.

Request examples

curl -X POST "https://api.subsidia.protypa.fr/engine/flows" \n -H "Authorization: Bearer $SUBSIDIA_API_KEY" \n -H "Content-Type: application/json" \n -d '{
"name": "Echeances de la semaine",
"timezone": "Europe/Paris"
}'

Responses

The flow in its full shape: id, name, enabled, version, timezone, concurrency, notifyPolicy, allowUnattendedOutbound, dailyTokenBudget, nodes, edges, triggers.

GET/engine/flow-templates

Ready-made flows you can start from.

API key or sessionreflex

Each template has an id, a name, what it is for (why), what it needs configured and its stepCount. Current ids include deadlines-watch, night-watch, piece-check, new-client-document, client-mail-triage, subcontractor-replies, weekly-review, follow-ups and service-watch.

Request examples

curl "https://api.subsidia.protypa.fr/engine/flow-templates" \n -H "Authorization: Bearer $SUBSIDIA_API_KEY"

Responses

The templates (templates array).

POST/engine/flows/from-template

Create a flow from a template.

API key or sessionreflex

The new flow is returned with its issues: a template deliberately leaves workspace-specific fields (which agent, which mailbox, which folder) empty, and those steps come back flagged. Fill them with PUT /engine/flows/:id/graph.

Request body

  • templateIdstringrequired
    An id from GET /engine/flow-templates.

Request examples

curl -X POST "https://api.subsidia.protypa.fr/engine/flows/from-template" \n -H "Authorization: Bearer $SUBSIDIA_API_KEY" \n -H "Content-Type: application/json" \n -d '{
"templateId": "deadlines-watch"
}'

Responses

The flow and its validation issues (flow, issues).

GET/engine/flows/:id

Get a flow with its graph, triggers and issues.

API key or sessionreflex

issues lists what is wrong with the graph. Each has a level (error or warning), a nodeKey, a code (missing-config, orphan, unguarded-outbound, cycle, no-trigger...), blocks (save or run) and a message. Errors that block run stop you enabling or starting the flow.

Path parameters

  • idstringrequired
    The flow id.

Request examples

curl "https://api.subsidia.protypa.fr/engine/flows/$ID" \n -H "Authorization: Bearer $SUBSIDIA_API_KEY"

Responses

{ flow, issues }.

PATCH/engine/flows/:id

Update flow settings: name, enabled, concurrency, notification policy.

API key or sessionreflex

Send only the fields to change. Enabling a flow with a blocking issue is refused with the list of issues, because a failure knowable at 4 pm should not wait until 4 am.

Path parameters

  • idstringrequired
    The flow id.

Request body

  • namestring
    New name.
  • descriptionstring
    New description.
  • timezonestring
    IANA time zone.
  • enabledboolean
    Switch the flow on or off.
  • concurrencystring
    What to do when a trigger fires during a run.
    skipqueueparallel
  • notifyPolicystring
    Whether a run that found nothing notifies anyone.
    alwaysonly-if-actionable
  • dailyTokenBudgetnumber
    Daily ceiling, null for none.
  • allowUnattendedOutboundboolean
    Waive the approval rule. Session only: an API key gets 403 session_required.

Request examples

curl -X PATCH "https://api.subsidia.protypa.fr/engine/flows/$ID" \n -H "Authorization: Bearer $SUBSIDIA_API_KEY" \n -H "Content-Type: application/json" \n -d '{
"enabled": true,
"notifyPolicy": "only-if-actionable"
}'

Responses

{ flow, issues }.

PUT/engine/flows/:id/graph

Replace the graph of a flow.

API key or sessionreflex

Saves all nodes and edges in one go (the previous graph is replaced, not merged). Work in progress is accepted: unwired steps and empty fields come back as issues that block switching the flow on, not saving. Only a graph the executor could not read back is refused: a cycle, an edge to a node that does not exist, a node feeding itself, two nodes sharing a key, an unknown type, a trigger with an input.

Path parameters

  • idstringrequired
    The flow id.

Request body

  • nodesobject[]required
    The steps.
  • edgesobject[]required
    The links.

Request examples

curl -X PUT "https://api.subsidia.protypa.fr/engine/flows/$ID/graph" \n -H "Authorization: Bearer $SUBSIDIA_API_KEY" \n -H "Content-Type: application/json" \n -d '{
"nodes": [
{
"key": "trigger",
"type": "schedule",
"config": {
"expr": "weekly@fri@17:00",
"windowHours": "7,21",
"misfire": "fire-once"
}
},
{
"key": "deadlines",
"type": "cortex.deadlines",
"config": {
"horizonDays": 14
}
},
{
"key": "gate",
"type": "gate",
"config": {
"mode": "actionable"
}
},
{
"key": "tell",
"type": "notify",
"config": {
"channel": "all",
"title": "Echeances",
"body": "{{deadlines.summary}}"
}
}
],
"edges": [
{
"fromKey": "trigger",
"toKey": "deadlines"
},
{
"fromKey": "deadlines",
"toKey": "gate"
},
{
"fromKey": "gate",
"toKey": "tell"
}
]
}'

Responses

{ flow, issues } after the save.

DELETE/engine/flows/:id

Delete a flow with its history.

API key or sessionreflex

Path parameters

  • idstringrequired
    The flow id.

Request examples

curl -X DELETE "https://api.subsidia.protypa.fr/engine/flows/$ID" \n -H "Authorization: Bearer $SUBSIDIA_API_KEY"

Responses

Deleted.

POST/engine/flows/:id/run

Run a flow now.

API key or sessionreflex

Queues a manual run and returns immediately; follow it with the run endpoints or the live stream. A manual run is subject to the same rules as any other: blocking issues refuse it, and an outbound step still waits for its approval. It works whether or not the flow is enabled.

Path parameters

  • idstringrequired
    The flow id.

Request examples

curl -X POST "https://api.subsidia.protypa.fr/engine/flows/$ID/run" \n -H "Authorization: Bearer $SUBSIDIA_API_KEY"

Responses

Queued.

Example response
{ "runId": "run_5d20" }

POST/engine/flows/:id/webhook

Start a flow from an outside system (webhook trigger).

Header x-reflex-secret (the trigger secret), no API key

The only Reflex route without an API key: the caller is a business application or a controller with no session, so the shared secret of the trigger is the credential. The flow needs a webhook trigger step with a secret; without one the route refuses (403 webhook_secret_required) rather than treating the URL as public. The secret is compared in constant time.

The JSON body you post becomes the trigger output, available to later steps as {{trigger.output}}. Send x-reflex-event-id to make a retry of the same fact start one run instead of two. Active hours, concurrency and the approval rule apply as for any trigger: this route opens no back door.

Path parameters

  • idstringrequired
    The flow id.

Headers

  • x-reflex-secretstringrequired
    The shared secret configured on the webhook trigger step.
  • x-reflex-event-idstring
    Your own id for the event (up to 200 characters). A second call with the same id starts nothing.

Request body

  • (any JSON)object
    Free-form payload handed to the flow.

Request examples

curl -X POST https://api.subsidia.protypa.fr/engine/flows/$ID/webhook \
-H "x-reflex-secret: $REFLEX_SECRET" \
-H "x-reflex-event-id: invoice-2026-0412" \
-H "Content-Type: application/json" \
-d '{"invoice": "2026-0412", "client": "Durand"}'

Responses

A run was started.

Example response
{ "started": 1 }

GET/engine/flows/:id/runs

Run history of a flow, newest first.

API key or sessionreflex

Path parameters

  • idstringrequired
    The flow id.

Query parameters

  • limitintegerdefault 30
    Up to 100.

Request examples

curl "https://api.subsidia.protypa.fr/engine/flows/$ID/runs?limit=10" \n -H "Authorization: Bearer $SUBSIDIA_API_KEY"

Responses

Run summaries. status is one of queued, running, awaiting-approval, succeeded, failed, cancelled, stalled.

Example response
{
"runs": [
{
"id": "run_5d20",
"status": "succeeded",
"triggeredBy": "schedule",
"actionable": true,
"tokensSpent": 3120,
"scheduledFor": "2026-10-09T06:30:00.000Z",
"startedAt": "2026-10-09T06:30:01.000Z",
"finishedAt": "2026-10-09T06:30:19.000Z",
"error": null,
"createdAt": "2026-10-09T06:30:00.000Z"
}
]
}

GET/engine/runs/:runId

One run with the result of every step.

API key or sessionreflex

The step-by-step record: for each attempt of each step, its status, input, output, logs, duration and tokens, plus the approvals the run raised.

Path parameters

  • runIdstringrequired
    The run id returned when the run was started.

Request examples

curl "https://api.subsidia.protypa.fr/engine/runs/$RUNID" \n -H "Authorization: Bearer $SUBSIDIA_API_KEY"

Responses

{ run }, with nodeRuns, approvals and flow: { id, name }.

POST/engine/runs/:runId/resume

Resume a run that ended badly.

API key or sessionreflex

Puts a failed, cancelled or stalled run back in the queue. Steps that already succeeded are not run again (their recorded output is replayed), so a three-hour ingestion that died on its last step does not start over.

Path parameters

  • runIdstringrequired
    The run id returned when the run was started.

Request examples

curl -X POST "https://api.subsidia.protypa.fr/engine/runs/$RUNID/resume" \n -H "Authorization: Bearer $SUBSIDIA_API_KEY"

Responses

Queued again.

Example response
{ "ok": true }

POST/engine/runs/:runId/cancel

Cancel a queued, running or waiting run.

API key or sessionreflex

Path parameters

  • runIdstringrequired
    The run id returned when the run was started.

Request examples

curl -X POST "https://api.subsidia.protypa.fr/engine/runs/$RUNID/cancel" \n -H "Authorization: Bearer $SUBSIDIA_API_KEY"

Responses

Cancelled.

Example response
{ "ok": true }

GET/engine/runs/:runId/stream

Follow a run live (Server-Sent Events).

API key or sessionreflex streams (SSE)

Events: run.started, node.started, node.log, node.finished (with status, summary, actionable, durationMs, tokens), run.finished and approval.requested. Each is sent as a data: line holding JSON. A : ping comment is sent every 15 seconds. Read it with fetch and a stream reader: EventSource cannot send the Authorization header.

Path parameters

  • runIdstringrequired
    The run id returned when the run was started.

Request examples

curl -N https://api.subsidia.protypa.fr/engine/runs/$RUNID/stream \
-H "Authorization: Bearer $SUBSIDIA_API_KEY"

Responses

text/event-stream. The stream only carries events emitted while you are connected; read the run with GET /engine/runs/:runId for what happened before.

GET/engine/approvals

Approvals waiting for a decision.

API key or sessionreflex

Everything parked in the inbox: what the step wants to do (summary), its exact payload (for an email, the message that would be sent), and when it expires. An API key can read the list but cannot decide: a person approves or rejects in the app, from the link in a notification, or through POST /engine/approvals/:id/decide with a signed-in session.

Request examples

curl "https://api.subsidia.protypa.fr/engine/approvals" \n -H "Authorization: Bearer $SUBSIDIA_API_KEY"

Responses

Pending approvals, newest first.

Example response
{
"approvals": [
{
"id": "apr_9c02",
"runId": "run_5d20",
"flowId": "flw_7a11",
"flowName": "Relance des factures",
"nodeKey": "send_mail",
"kind": "approval",
"summary": "Send 3 reminder emails",
"payload": {},
"expiresAt": "2026-10-12T06:30:00.000Z",
"createdAt": "2026-10-09T06:30:15.000Z"
}
]
}

POST/engine/approvals/:id/decide

Approve or reject a waiting step (signed-in person only).

User session (JWT). API keys are refused.

Refused for API keys by design. The decision is recorded with the person who made it. The same decision logic serves the in-app panel and the links sent in emails and push notifications.

Path parameters

  • idstringrequired
    The approval id.

Request body

  • decisionstringrequired
    What to do.
    approvereject
  • notestring
    Optional comment.

Request examples

curl
curl -X POST https://api.subsidia.protypa.fr/engine/approvals/$ID/decide \
-H "Authorization: Bearer $USER_JWT" \
-H "x-workspace-id: $WORKSPACE_ID" \
-H "Content-Type: application/json" \
-d '{"decision": "approve"}'

Responses

Recorded; the run continues (approve) or stops that branch (reject).

Example response
{ "ok": true }

Errors

  • 401No user session. An API key cannot reach this route.

GET/engine/connections

List the services registered for flows.

API key or sessionreflex

Connections are external services a person registered once in the app (Reflex, Connections) with a base URL and credential headers, used by the connection.call step. Header values are never returned. Creating, changing, testing and deleting connections requires a signed-in person, because they hold credentials; a step can only supply a path, so a flow cannot send the credentials anywhere else.

Request examples

curl "https://api.subsidia.protypa.fr/engine/connections" \n -H "Authorization: Bearer $SUBSIDIA_API_KEY"

Responses

The connections, without their secret header values.

Getting notified instead of polling

Two webhook events are pushed to your own system so you do not have to poll: flow.run.finished (run id, flow, status, whether it was actionable) and flow.approval.requested (run id, step key, approval id). Both carry identifiers only; fetch the run for details.

Frequently asked

Can a flow send emails on its own?

Only if you waive the approval, which requires a signed-in person, is off by default and is logged. With the default setting every mail.send, http.call and connection.call needs an approval in front of it, and mail.reply always stops on the exact message.

My machine was off over the weekend. What runs on Monday?

One catch-up run per flow (policy fire-once), not one per missed slot. Use skip on a trigger if a late run is useless.

Does a run cost questions?

Background work does not consume your question allowance; it is metered internally. A flow can still have a dailyTokenBudget as a guard rail.

Can I run Reflex in the cloud?

Not today. Flows can be edited through the API, but the scheduler only runs on local-mode installations.

Related