Workflows: governed automations and deliveries

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.

SOURCETRANSFORMATIONFILESegmentexplicit projectionProjectionreference · e-mail · cityFormattingCSV · PGP-encryptedSFTP depositdeclared destination · piipinned host keye-mail (pii) → tech targetrefused at compile timeHTTP warehousedeclared destination · technothing leavesLog: field names · count · file fingerprint — never a value
The batch follows the only authorised branch. The other one is refused before it exists: the class of a column and the class of a destination are compared at compile time.

A graph, batches, versions

Typed nodes, typed ports, and a compilation that refuses before it 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 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.
  • The branches of a graph run in a deterministic sequence inside the same run: the order depends neither on the clock nor on load, and memory stays flat.
What tells 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 share their 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.

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 timezone — “two in the morning” means two in the morning at the client’s, including on the night the clocks change. One occurrence produces exactly one run, whatever the number of scheduler instances.
  • Manual: an API call with an idempotency key. 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 element 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 answer the same thing: probing teaches 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 element 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 at the partner’s 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 element, even across two windows. The elements live in the database, never in a job, and disappear with their run.

Sources

Every source declares an explicit projection. A workflow never reads “the whole contact”.

  • Segment members — 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: names of fields, never values, and the fingerprints of the chain so it can be verified outside Missivia.
  • Contacts are read in batches, on a cursor: never a whole table in memory.
Time windows count from the start of the run

“Since” and “until” accept the present 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 clock when the node runs: resuming after an incident reads exactly the same window, and a replayed deposit stays idempotent.

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, an unsafe pattern are refused at writing time, with their 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 timezone.
  • A hashed column becomes technical: a fingerprint of an address is not an address. It may leave for a technical target — which is precisely the use: giving a partner a matching key without giving them 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 random, 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 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: it is refused earlier, when the source projects its fields. 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 name preserved), Excel — one sheet, written row by row: a million rows do not grow the memory.
  • 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 a content.
  • PGP encryption for the recipient’s public key, or age.
  • 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 tells which ones are stored.
  • Every destination carries the data class it may receive: technical or personal. There is no “health” class: no destination can receive a health attribute.
  • 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 goes through the guard against internal targets at deposit time, not only at declaration. S3: endpoint pinned on the destination, deterministic object key, server-side encryption asked for 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 outbound IP address, for all three protocols

Partners filter by source address. Every SFTP, HTTP or S3 deposit — and every destination verification — leaves through a proxy placed inside the perimeter, on a host with a fixed address: the partner has one address to allow, the same for all three protocols. TLS and SSH are established on top of the tunnel, against the pinned endpoint or host key: the proxy reads nothing of what passes.

There is no automatic fallback: if the proxy does not answer, the deposit fails and says so, rather than leaving through an address the partner does not allow without anyone knowing.

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 carries 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 — nor a health attribute.
  • Idempotence is at the grain of the element: a resume does not replay an element that already succeeded. A business refusal on one element does not fail the batch: the element leaves marked “skipped”, and the run continues.
  • From a journey, a step starts a workflow for the current contact, either moving on at once or waiting for the outcome — bounded: 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.

Governance

Four rules, enforced at compile time and again at run time — by the server, not by a review.

  • A health-classified attribute never leaves a space through a workflow.
  • A segment derived from a health criterion cannot be a source.
  • 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 names of the fields, 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 reads “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.
  • Bounds per space — 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 says 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 thirty days by default — floor one day, ceiling: the space’s contact retention period.