What leaves a space is declared, classified, logged
A workflow is a versioned graph of typed nodes through which batches of items flow: a source, transformations, a file produced on the way, a destination. A run takes seconds or minutes. It is not the journey engine – which follows one contact for weeks – but it rests on the same building blocks. And what sets it apart from a generic orchestrator fits in one sentence: a Missivia node knows what kind of data it handles, and what that data is allowed to become.
A graph, batches, versions
Typed nodes, typed ports, and compilation that refuses before anything runs.
- A definition is three lists: triggers, nodes with a stable identifier, edges. The graph is acyclic; ports are typed – items, file, none – and the compiler refuses a connection that does not match.
- A draft is stored without compiling: an editor saves work in progress. A published version has been compiled: the governance rules are applied when you decide this graph will run, not the night it does.
- The node catalogue is published with the JSON Schema of every configuration, generated from the very schemas the API validates against: an editor cannot drift from the engine.
- Each node persists its result before the next one starts. An interrupted run resumes at the first node that has not finished; a node that succeeded is never replayed. The partner receives one file, not two.
- Every node chooses its behaviour on error – fail, skip, divert – and its number of attempts, spaced out over time.
- Independent branches of a graph run in parallel, within a fixed limit and under a per-run memory budget; outputs stay deterministic – a join assembles its inputs in port order, never in completion order – and recovery after an incident replays no actions.
What sets a workflow apart from a journey
A journey is a state machine per contact, alive for weeks: waits, branches, conversion goals. A workflow processes batches and produces files, in seconds or minutes. The two models are not merged – one engine would do both badly.
They do, however, share the same foundation: the expression engine, file storage, credential encryption, the guard against internal network targets, the audit log and the key classes. Two bridges connect them: a journey step that starts a workflow, and a workflow node that enrols contacts in a journey.
Editor: try, resume, start from a recipe
You build a workflow by watching it run, without delivering anything; you repair a run without redoing everything.
- Try a step: from the editor, the chosen node and its ancestors run on the current draft, and its output data is shown under the node. Each source returns at most twenty items by default, and a hundred at most.
- A trial has no side effect: no action, no destination (e-mail included) and no sub-workflow is executed; a fetched file stays in place on the partner’s side. Trials appear neither in the run history nor in the statistics, and are purged after 24 hours. They count towards the space’s run quota.
- Resume a failed run from the failing node: a new run reuses the outputs already produced outside the part of the graph downstream of that node – no re-extraction, no re-deposit, no new log entry for them – and replays the failed node and what follows under the rules of the first pass. The original run stays intact.
- A resume is refused, with its reason, when it cannot be safe: definition changed since, health governance changed, outputs already purged, input file unavailable, a downstream sub-workflow that would have to be restarted. It does not extend the retention of anything.
- Recipes: starter templates, ready to adapt. The first one alerts the space’s operational address by e-mail whenever a run fails, never including the error message. A recipe creates nothing by itself and carries no address: the connection is declared and verified, then the workflow is published, like any other.
- The editor keeps a draft history (a hundred steps, undo and redo) and copies, pastes and duplicates nodes with their internal links, with the keyboard or mouse. The clipboard is internal to the editor and to the open workflow: never the system clipboard, never another workflow. No shortcut is captured inside an input field.
Triggers
Six ways to start. A paused workflow, or one with no published version, is never woken up.
- Schedule: a five-field expression, evaluated in the space’s time zone – “two in the morning” means two in the morning for the customer, including on the night the clocks change. One occurrence produces exactly one run, whatever the number of scheduler instances. An optional business-day calendar sets aside Saturdays, Sundays and public holidays (metropolitan France or Alsace-Moselle): the occurrence is skipped, or moved to the next business day at the same time, never doubling up on a day that already has its own run.
- Manual: an API call with an idempotency key, accompanied, if the workflow expects one, by a file uploaded just before. A workflow that does not declare this trigger refuses the call.
- Event, business or platform – contact created, list subscription, consent granted, unsubscribe, enrolment completed, campaign completed, export completed, reputation severity changed, workflow failed – with an optional condition evaluated before the item joins the batch, and a grouping window of zero to one hour that gathers the events of one interval into a single run.
- Inbound URL with a token: an inbound HTTP call whose JSON body becomes the first batch. The token is shown once, stored hashed, rotatable, revocable; a signature can be required. Nothing of the body is ever logged, and an unknown, revoked or paused token all get the same response: probing reveals nothing.
- From a journey: a step starts the workflow for the current contact – because the workflow allowed it by declaring this trigger.
- From another workflow: a sub-workflow called with the current batch, under the same consent: the callee declares that it accepts being called.
The trigger batch is read by a single node
Event, inbound URL, journey step or calling workflow: the four triggers that bring a batch have it read by a single source node, and every item has the same shape – the event, the contact, the payload. The payload is classified personal without exception: its content is written by the caller, not by us, and an omission would show up on the partner’s side rather than at compile time. To deliver it to a technical target, you pseudonymise it, or you declare the destination as personal.
Deduplication is by event: two deliveries of the same event produce one item, even across two windows. The items live in the database, never in the queue, and disappear with their run.
Sources
Every source declares an explicit projection. A workflow never reads “the whole contact”.
- Segment members – outside a health workflow, refused when the segment derives from a health criterion: membership alone would reveal the data.
- List subscribers, and contacts filtered by an expression. Filtering is not exporting: the expression may read a column that is not projected.
- Business events over a time window, with their opaque references; every payload field is personal.
- Statistics aggregates, from the same closed vocabulary as the dashboards: a count masked by the display threshold comes out empty, not in the clear.
- Message log: opaque identifiers, statuses, counters, dates – no recipient address, no rendered subject, no provider error text.
- Audit log: field names, never values, and the fingerprints of the chain so it can be verified outside Missivia.
- Contacts are read in batches, with a cursor: never a whole table in memory.
- A file, fetched from a connection or uploaded at launch: see Incoming files.
Time windows are counted from the start of the run
“Since” and “until” accept the current instant, an offset relative to the start of the run – seven days, thirty-six hours – or a date. The reference is the start of the run, not the time at which the node runs: resuming after an incident reads exactly the same window, and a replayed deposit stays idempotent.
Incoming files
A batch of leads held by a partner, a return file from a provider, a file supplied by hand: a workflow fetches it, decrypts it, reads it into columns, turns it into contacts.
- Four entry points: SFTP, S3-compatible and HTTP on a declared and verified connection, or an upload supplied when a manual run is started. One file per run.
- Once read, the remote file is left in place, moved to a processing folder or deleted, as configured. An HTTP fetch follows no redirect, and a file unchanged since last time is not fetched again.
- The size is bounded before and during reading, by a platform limit published on the space’s record; reading into columns is bounded too.
- Every incoming file writes an import entry to the audit log (name, size, fingerprint), and the consumed input is recorded on the run: type, size and fingerprint, the name being shown to customer roles only.
- An upload serves a single run; if not consumed, it is purged after 24 hours, content included. Once consumed, it follows the run’s retention.
- OpenPGP decryption with a space key, generated on the platform or imported – the private key is never returned – or with a passphrase taken from the vault. The file keeps the class of its source connection, and reading it into columns never lowers it.
Transformations and functions
No code node. Typed nodes, and a closed vocabulary of functions.
- Projection (selection, renaming, composed templates), filter, sort, dedupe, limit, re-chunking into batches.
- Aggregation by one to three keys (count, sum, average, minimum, maximum, distinct count); join of two streams by key; routing of each item to a named output by the value of one column; fan-out of the same batch to several branches; union of branches.
- The class of a composed column is the highest of the columns it cites: a first name and an identifier yield a personal column. A count is technical – counting is not disclosing. A join brings the right-hand columns in with their own class: joining an address onto a technical batch makes it personal.
- Functions – dates, strings, arrays, numbers, logic, hashing – are resolved at compile time in a closed registry: an unknown name, a wrong arity or an unsafe pattern is refused at authoring time, with its position in the author’s text.
- No function reads the network, a file or the system clock: “now” is the reference instant of the run, and the first of the month is computed in the space’s time zone.
- A hashed personal column becomes technical: a fingerprint of an address is not an address. It may leave for a technical target – which is precisely the point: giving a partner a matching key without handing over the people. The secret of a keyed hash is designated by a name, derived per space, and is never written in the expression.
What the vocabulary does not contain – and what a graph cannot add to it
No code evaluation, no user-defined function, no HTTP call, no file access, no randomness, no system clock, no regular expression built from a value, and nothing that decrypts or reverses. The registry is closed (forty-two functions in six families); the absence of all this is verifiable, and it is tested. Extending the vocabulary is a product release, not a workaround in a graph.
Hashing is not a way to get health data out: the fingerprint of a health column stays classified as health, and only leaves encrypted, to a health connection. The same engine serves triggers, journeys and templates; the registry is published as API data, and the documentation is generated from it – a wrong example fails a test.
Formats and encryption
A file in the partner’s format, produced as a stream, with a deterministic name.
- CSV (delimiter, quotes, header, UTF-8 or Latin-1), JSON Lines, JSON, XML (configurable root and item elements, column names preserved), Excel – one sheet, written row by row: a million rows do not increase memory use.
- Gzip compression, or a zip archive of several files from the same run. The archive carries the highest class of what it contains, and says what it contains – names and sizes, never contents.
- PGP encryption for the recipient’s public key, with an OpenPGP passphrase taken from the vault, or age.
- Protection against spreadsheet formulas in CSV, on by default: a cell that would start a formula is written preceded by an apostrophe, a number never is. It can be switched off for a machine consumer; the API’s CSV exports always keep it.
- The file name depends on the run, not on the clock at deposit time: a replayed deposit yields the same name, hence one file.
- Encrypting does not change the class of the data: what authorises the exit is the declared destination, not the protection of the transport.
Declared destinations
Nothing leaves for a target that has not been declared, once, by an administrator of the space.
- Four types: SFTP, HTTP, S3-compatible, e-mail with attachment. Credentials are encrypted per space and never returned; a fingerprint shows which ones are stored.
- Every destination carries the data class it may receive – and, as a fetch connection, the class of what comes from it: technical, personal or health. The health class only exists for an SFTP connection, subject to an attestation: HTTP, S3 and e-mail never receive health data (see Health data).
- Changing a connection’s host erases its stored credentials, unless they are entered again in the same step; any change of target resets the verification, and nothing is published or run on that connection before a new successful verification.
- SFTP: the host key fingerprint is mandatory and compared before authentication – no password is ever sent to a host that does not match. The file is written under a temporary name, then renamed: a partner polling the directory never picks up a half-written file.
- HTTP: an idempotency key per run and per node; the host is checked again against the internal-target guard at deposit time, not only at declaration. S3: endpoint pinned on the destination, deterministic object key, server-side encryption requested at write time.
- E-mail: the file goes as an attachment, through the internal relay, to the addresses declared on the destination. The node has no recipient field: changing who receives an extraction is an act of space administration, not a graph edit.
- Declaring destinations requires the administration key class; building and running workflows is open to the operator class. A destination is verified before publication – connection, host key, directory, nothing written – and cannot be deleted while a published version names it.
One fixed outbound IP address, on request, for all three protocols
Some partners filter by source address. For them, the platform can route every SFTP, HTTP or S3 deposit – and every destination verification – through a proxy placed within the perimeter, on a host with a fixed address: the partner then has one address to allow, the same for all three protocols. This proxy, operated by the platform within its perimeter, is a deployment option set up under the contract when a partner requires it; without it, deposits leave through the hosting provider’s addresses. TLS and SSH are established on top of the tunnel, against the pinned endpoint or host key: the proxy reads nothing of what passes.
Once the proxy is in place, there is no automatic fallback: if it does not answer, the deposit fails and says so, rather than silently leaving through an address the partner does not allow.
Space secret vault
A passphrase shared with a partner, a signing key, a header token: the workflow uses them without ever showing them.
- A value is supplied, or generated by the platform and shown only once. It is never returned: a keyed fingerprint, specific to the space, tells two versions apart without allowing a value to be tested.
- A secret is never written into the definition: it is referred to by name, only in the configuration fields meant for it – never in an expression, a path or a file name.
- Controlled uses: every secret declares what it is used for – OpenPGP passphrase encryption or decryption, keyed hashing, HTTP header, HTTP signature – and can be bound to specific connections. An undeclared use is refused at compile time.
- HTTP calls: secret headers set on every call made through a connection, HMAC-SHA256 signature of the bytes sent, timestamped if the partner asks for it.
- Rotation with a grace period for decryption; a secret used by a published version cannot be deleted, and the refusal states what refers to it. At run time, versions are pinned at start and values are removed from every error and every log.
- Encryption bound to its row: a value copied into another space, another secret or another version no longer decrypts. Platform operating roles see names and uses, never a value.
Actions and bridges with journeys
Nodes that write into the space – a different question from leaving it, with a different answer.
- Create or update a contact (with a declared legal basis to create a record), set attributes, enter or leave a list, emit a business event, enrol in a published journey.
- Every action goes through the business function that already handles it: suppression list, inactive contact, quotas, consent required by a journey apply as they do to an API call. No action writes a consent – a consent is collected, not computed. A health attribute is only written in a health workflow, for a contact that already carries the dedicated consent.
- Idempotence applies item by item: a resume does not replay an item that already succeeded. A business refusal on one item does not fail the batch: the item leaves marked “skipped”, and the run continues.
- From a journey, a step starts a workflow for the current contact, either moving on immediately or waiting for the outcome – for a bounded time: an enrolment does not stay stuck because a partner is down.
- A called sub-workflow returns columns presumed personal, re-checked at run time: a child cannot downgrade data by routing it through itself.
- Loops are bounded in both directions: a run born from an event cannot re-emit it, chaining depth is limited, and a cycle of calls between workflows is refused at compile time.
Health data
An authorised person can bring health data into and out of a workflow. The framework is closed, and the server enforces it.
- A workflow must be declared as handling health data, by an authorised person; publishing, activating, running and downloading a produced file require the same authorisation. It cannot be triggered from a journey or called from a workflow that does not handle health data.
- Health data only comes in through an SFTP connection classified as health, and only leaves encrypted, to such a connection: HTTP, S3 and e-mail never receive it.
- Classifying a connection as health requires a written attestation: the server sits within a health data hosting perimeter, under the data controller’s responsibility. The platform records it; it does not claim to verify it. Any change of target – host, port, account, host-key fingerprint – invalidates it: nothing is published or run before a new attestation and a new verification.
- Whatever decides on a health column – a filter, a sort, a join, a grouping – makes the whole flow health-derived, all the way through, the cohort’s addresses and even a mere count downstream included. Hashing a health column never declassifies it.
- Health columns are removed from samples before anything is stored; working files are encrypted at rest; the runs of a health workflow do not exist for platform operating roles.
- A health value is only written to a contact that already holds a granted, dedicated consent; otherwise the item is rejected, with nothing written.
Governance
Rules enforced at compile time and enforced again at run time – by the server, not by a review.
- Outside a workflow declared as handling health data, a health-classified attribute never leaves a space, and a segment derived from a health criterion cannot be a source. In a health workflow, health data only leaves encrypted, to an SFTP connection classified as health.
- Nothing goes anywhere that has not been declared as a destination of the space.
- A personal column only reaches a destination allowed to receive it; a technical destination only receives technical fields, opaque identifiers and aggregates.
- Every deposit writes an export entry to the audit log: the field names, the item count, the destination and the fingerprint of the file – never a value. A refusal observed at run time – a segment that became health-derived since publication, a downgraded destination, a blocked network target – writes a denied entry, and nothing leaves.
- The samples of a run are masked by the class of the key that reads them: an operator key sees technical columns and counts, the rest shows as “masked”. The real file is downloaded only with an administration key. The platform operator observes a space’s runs with no sample, no destination and no file name.
- Per-space bounds (runs per day, declared destinations, items, size, duration), operating metrics with no node identifier, and an alert on repeated failure that never carries the error message: it could name a partner’s host.
The bounds of a run, by default
- 200,000 items per run, 200 MiB per produced file, 15 minutes per run, one run at a time per space.
- 500 runs started per day and 20 declared destinations per space – two bounds the operator sets, zero meaning “no bound”. A quota refusal states when the counter reopens.
- Chaining depth limited to three, whatever the path – sub-workflow, journey step, emitted event.
- Alert on the third consecutive failure; the counter lives in the database, so a redeploy does not forget that the midnight deposit has been failing for three nights.
- Runs, samples and produced files kept for thirty days by default – minimum one day, maximum the space’s contact retention period.