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 node | Starts the flow when | Notes |
|---|---|---|
schedule | The time expression is due. | Grammar below. At least 5 minutes between runs. |
manual | Someone presses Run, or POST /engine/flows/:id/run is called. | Never fires by itself. |
webhook | An outside system posts to the flow URL. | Needs a shared secret. See below. |
folder.changed | A document arrives or changes in a watched folder. | The folder is configured in the Cortex synchronisation settings. |
mail.received | A 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.
| Type | Category | Effect | What it does |
|---|---|---|---|
schedule | trigger | read | Starts on a time expression. |
manual | trigger | read | Starts when someone presses Run. |
webhook | trigger | read | Starts when an outside system posts to the flow URL. |
folder.changed | trigger | read | Starts when a document lands in a watched folder. |
mail.received | trigger | read | Starts when mail arrives in a connected mailbox. |
agent.run | agent | read | One full agent turn: its documents, memory and (optionally) tools. A silence word (default RAS) means nothing to report. |
agent.debate | agent | read | The workspace panel argues a question and converges on an answer. |
cortex.search | cortex | read | Passages that answer a question, with their sources. |
cortex.read | cortex | read | The text of one document, by id. |
cortex.inventory | cortex | read | The pieces a dossier contains, as a plain list with no model. It is what lets a flow say what is missing. |
cortex.ingest | cortex | local-write | Files a text from a previous step (or the trigger document) in a Cortex dossier. Same content means same piece: nothing is duplicated. |
cortex.deadlines | cortex | read | Reads the date columns of your registers (CSV, XLSX) and returns the approaching deadlines, exact rows, no model. |
light.ask | cortex | read | Asks Light: answers strictly from the documents with citations, or an honest refusal. |
discovery.night | cortex | read | Re-reads dossiers under watch overnight and prepares the morning questions: missing pieces, overdue deadlines, contradictions. |
gate | logic | read | Stops the branch when the previous step had nothing worth saying (modes actionable, covered, uncovered, always). |
branch | logic | read | Sends the flow one way or the other; the conditions are on the links. |
delay | logic | read | Waits 1 to 240 minutes. |
notify | effect | local-write | Tells you, through a configured channel or the in-app inbox. |
file.write | effect | local-write | Saves a result to a folder on the machine. |
mail.draft | effect | local-write | Writes an email and stops. Nothing is sent. |
http.call | effect | outbound | Calls an external URL. A GET reads, anything else writes. |
connection.call | effect | outbound | Calls a service registered under Connections, with its stored credentials. |
mail.send | effect | outbound | Sends an email. |
mail.reply | effect | outbound | Replies 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.human | human | read | Asks you a question with context. Your answer is filed in the dossier, then the run resumes. |
approval | human | read | Parks 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
requiresApprovalin 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.
reflexRequest 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.
{ "enabled": true, "flows": 6, "active": 4, "pendingApprovals": 2}GET/engine/node-types
The step catalogue, with each step configuration fields.
reflexThe 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.
{ "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.
reflexRequest 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.
{ "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.
reflexCreates 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
namestringrequiredDisplay name.descriptionstringFree text.timezonestringdefaultEurope/ParisIANA 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.
reflexEach 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.
reflexThe 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
templateIdstringrequiredAnidfromGET /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.
reflexissues 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
idstringrequiredThe 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.
reflexSend 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
idstringrequiredThe flow id.
Request body
namestringNew name.descriptionstringNew description.timezonestringIANA time zone.enabledbooleanSwitch the flow on or off.concurrencystringWhat to do when a trigger fires during a run.skipqueueparallelnotifyPolicystringWhether a run that found nothing notifies anyone.alwaysonly-if-actionabledailyTokenBudgetnumberDaily ceiling,nullfor none.allowUnattendedOutboundbooleanWaive the approval rule. Session only: an API key gets403 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.
reflexSaves 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
idstringrequiredThe flow id.
Request body
nodesobject[]requiredThe steps.edgesobject[]requiredThe 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.
reflexPath parameters
idstringrequiredThe 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.
reflexQueues 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
idstringrequiredThe 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.
{ "runId": "run_5d20" }POST/engine/flows/:id/webhook
Start a flow from an outside system (webhook trigger).
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
idstringrequiredThe flow id.
Headers
x-reflex-secretstringrequiredThe shared secret configured on the webhook trigger step.x-reflex-event-idstringYour own id for the event (up to 200 characters). A second call with the same id starts nothing.
Request body
(any JSON)objectFree-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.
{ "started": 1 }GET/engine/flows/:id/runs
Run history of a flow, newest first.
reflexPath parameters
idstringrequiredThe flow id.
Query parameters
limitintegerdefault30Up 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.
{ "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.
reflexThe 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
runIdstringrequiredThe 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.
reflexPuts 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
runIdstringrequiredThe 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.
{ "ok": true }POST/engine/runs/:runId/cancel
Cancel a queued, running or waiting run.
reflexPath parameters
runIdstringrequiredThe 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.
{ "ok": true }GET/engine/runs/:runId/stream
Follow a run live (Server-Sent Events).
reflexEvents: 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
runIdstringrequiredThe 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.
reflexEverything 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.
{ "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).
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
idstringrequiredThe approval id.
Request body
decisionstringrequiredWhat to do.approverejectnotestringOptional comment.
Request examples
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).
{ "ok": true }Errors
- 401No user session. An API key cannot reach this route.
GET/engine/connections
List the services registered for flows.
reflexConnections 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.