Associate people and purposes with jobs
For: tenant admins and governance reviewers.
Start a job for a person
oz0 run acme-triage "Customer says they were charged twice" --data-subject [email protected]
In the runtime API, set data_subject in StartJob, and data_subjects for more people. The subject is any ID that names the person in your systems: an email address, a customer number. The platform stores only a hash of it: an HMAC with a key the cluster's master key derives for the tenant. Without that key nobody can test a list of candidate addresses against the hashes in the audit log, an export or a backup.
The hash is of the subject's normal form, so every spelling of one ID reaches the same person, and two IDs stay two people. For every ID: fullwidth letters and digits (alice) are read as their usual width, an accent written as one character or two is one, invisible characters such as a zero-width space are left out, and runs of spaces are one. Then, by the kind of ID:
| The ID | Also the same | Kept apart |
|---|---|---|
A mail address (local@domain, or anything after mailto:) | letters in any case: Mailto:[email protected] is [email protected] | another address |
A phone number written as one (a leading +, or tel:) | spaces, dashes, dots, slashes and brackets: +46 70-123 45 67 is +46701234567 | a number with other digits |
| Anything else (a customer number, a record ID, a name) | letters in another case (001A000000abcDE and 001a000000ABCde), other separators (123-456 and 12-3456) |
Case and separators are kept outside addresses and phone numbers because they are often what tells two record IDs or customer numbers apart: an export or a find by one person's ID must never return another's data. Give a phone number with its + and country code, or with tel:, so that its spellings meet.
An erasure, a restriction or an objection by any spelling of one ID reaches the jobs started under the others. A subject that is empty once spaces and invisible characters are left out is refused.
Jobs recorded before this release under a spelling that is not in normal form keep the hash of that exact spelling: subject erase, subject find and a restriction or objection by that spelling still reach them, and a start checks restrictions under both.
A job started by a trigger (a webhook, a routine, an event source, a chat channel) gets its people from the trigger's mapping rule.
What happens:
- The edge makes the job a key of its own (AES-256), wraps it with a key of each person's, and encrypts the job's input with it before Temporal sees it.
- Every node that runs a step of the job, or of its child jobs, encrypts what it writes with the same key: step inputs and outputs, the answer, the summaries that name each step, pending approvals with their arguments. Large payloads in External Storage are encrypted too. Files the job's tools pass between them are sealed with the tenant's key instead (where payloads are encrypted), readable only by the job's tree, and deleted by the erasure rather than made unreadable by it.
- What the edge writes to the job is encrypted the same way: an approval's decision and comment, a question's answer. So are the notes agents pin to the job tree's board, and a chat's title and preview.
- The edge holds the keys, sealed with the cluster's master key, or its secrets key where the roles run apart. It decrypts for those who may see a job's content, so traces, steps, answers and approvals look as usual in the CLI and the console.
- The audit entries about the job are marked with the person's hash.
Several people in one job
A job can be about up to 8 people:
oz0 run acme-orders "Refund order 1182" --data-subject [email protected] --data-subject [email protected]
The job's key is wrapped with each person's key. Erasing any of them erases the whole job, for all of them, unless its agent's keep-for period defers it: then it is erased when the period ends. The others' data in that job goes with it. Whether erasing one person may erase a record that also concerns another, and what that person should be told, is a legal question the platform does not answer; this is what it does. Audit entries, gateway records and kept eval cases name every person of the job.
A conversation keeps the people of its first turn.
People from triggers
A webhook, a routine, an event source or a chat channel starts jobs without anyone naming a person. A mapping rule names them from what the trigger carries:
oz0 subject rule set webhook orders --pointer /buyer/email --pointer /seller/email
oz0 subject rule set channel support-room --cel 'identity.account'
oz0 subject rule test webhook orders --payload order.json
| Kind | Name | What the rule reads |
|---|---|---|
webhook | the flow | payload: the request's JSON body |
event | the event source | payload: {body, event}, the delivery's body and the event its source's function made of it; the delivery's own routing job has no event yet, so a rule that points into /body names the same people for the delivery and for every job its events start |
routine | the routine | payload: {input, body}, the job's input and what fired it; a fire on the routine's schedule has its input (the routine's own) and an empty body, and gets its people when it starts to run, before its first step |
channel | the binding | payload: {text, room, workspace}; identity: the author's {id, name, account} |
Each of up to 8 expressions is a JSON pointer (RFC 6901) or a CEL expression with a cost limit, yielding a string or a list of strings; the union, at most 8 people of at most 320 bytes each, are the job's.
An expression that fails or yields nothing adds no one, and the start's audit entry counts the people and names the problems (never the people). A rule that yields more than 8 refuses the start. subject rule test tries a rule on a sample and changes nothing. Rules name fields, never a person; setting one is audited (subject.rule.set).
Every trigger checks job IDs as StartJob does: a job ID used for someone's job is refused for another's.
One job ID, one set of people
A job ID that was used for people stays theirs: starting a job under that ID for anyone else, or without them, is refused (FailedPrecondition), and so is using it again after one of them was erased. Otherwise the nodes would encrypt the new job with the first job's key, and erasing those people would delete the new job too. Start the new job under a new ID; without --id, every job gets one.
Purposes and legal bases
An agent's definition declares what its jobs are for, by purpose IDs (purposes: [support, quality] in its YAML, see the agent definition). A start may name one of them:
oz0 run acme-triage "Customer says they were charged twice" --purpose support
oz0 channel bind helpdesk --channel slack --agent acme-triage --purpose support
In the runtime API it is purpose in StartJob; a routine takes purpose:, and a flow's webhook or event trigger trigger.purpose. The tenant keeps its own lists: legal bases (an ID and the tenant's own description) and purposes, each with a legal basis from that list. An agent may have its own legal basis for one of its purposes, in place of the purpose's.
oz0 inventory purposes set --legal-basis contract="Our contract with the customer" \
--purpose support="Answering customers' questions" --purpose-basis support=contract --reason "DPO review 2026-10"
oz0 inventory legal-basis acme-triage support contract --reason "DPO review 2026-10"
oz0 inventory purposes
What the platform checks:
- A start names a declared purpose, or none. A purpose the agent does not declare is refused before the job starts (
FailedPrecondition), with the fix; so is a channel binding, routine or flow trigger that names one (atplugin lint, at save, and again when it fires). - The job carries its purpose: in its memo,
Job.purpose, itsrun.startaudit entry, and the inventory's export. A retry keeps it. - Starts without a purpose are counted per agent and day, never per person. An agent that declares purposes and still gets jobs without one is a finding (
jobs_without_purpose), and so are a declared purpose the tenant does not list and one without a legal basis (the inventory's purposes). - A legal basis changes only with a reason. Giving, changing or removing a legal basis is a governance change: it takes
--reason, the audit log keeps it (inventory.purposes,inventory.legal_basis), and with four eyes on, a change or removal of a basis waits for another admin. So does a new description of a basis that a purpose or an agent's own basis names, since that is what the basis means.
A basis an agent's own basis names cannot be removed from the list until that is changed. Purposes' descriptions, a purpose without a basis, and a basis nothing names apply at once, without a reason. - An approval applies to the lists it was asked for. If the lists change while a request waits, approving it is refused (
request the change again), so an approval never undoes a later edit.
What it does not decide: which purposes and legal bases exist, how they are worded, and whether a basis fits a purpose. The lists ship empty and no wording is suggested; they are the tenant's, and the decision is yours and your data protection officer's. The platform does not check the tenant's list at start (only the agent's declared purposes), and a job without a purpose still runs.
Personal data
Start a job for one or more people, find what you hold about them, give them a readable copy, restrict or stop processing at their request, keep a contestable record of decisions about them, keep what a law asks you to keep, and erase the rest by destroying keys.
Find and export personal data
Find and export personal data for tenant admins and governance reviewers.