---
title: "Associate people and purposes with jobs"
description: "Associate people and purposes with jobs for tenant admins and governance reviewers."
canonical_url: "https://docs.oz0.ai/govern/personal-data/associate-people"
---
# Associate people and purposes with jobs

> Associate people and purposes with jobs for tenant admins and governance reviewers.

For: tenant admins and governance reviewers.

## Start a job for a person

```bash [Terminal]
oz0 run acme-triage "Customer says they were charged twice" --data-subject alice@example.com
```

In the [runtime API](https://docs.oz0.ai/build/apps/runtime-api), set `data_subject` in `StartJob`, and `data_subjects` for [more people](https://docs.oz0.ai/govern/personal-data/associate-people#several-people-in-one-job). 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 (`ａｌｉｃｅ`) 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:

<table>
<thead>
  <tr>
    <th>
      The ID
    </th>
    
    <th>
      Also the same
    </th>
    
    <th>
      Kept apart
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      A mail address (<code>
        local@domain
      </code>
      
      , or anything after <code>
        mailto:
      </code>
      
      )
    </td>
    
    <td>
      letters in any case: <code>
        Mailto:Alice@Example.com
      </code>
      
       is <code>
        alice@example.com
      </code>
    </td>
    
    <td>
      another address
    </td>
  </tr>
  
  <tr>
    <td>
      A phone number written as one (a leading <code>
        +
      </code>
      
      , or <code>
        tel:
      </code>
      
      )
    </td>
    
    <td>
      spaces, dashes, dots, slashes and brackets: <code>
        +46 70-123 45 67
      </code>
      
       is <code>
        +46701234567
      </code>
    </td>
    
    <td>
      a number with other digits
    </td>
  </tr>
  
  <tr>
    <td>
      Anything else (a customer number, a record ID, a name)
    </td>
    
    <td>
      
    </td>
    
    <td>
      letters in another case (<code>
        001A000000abcDE
      </code>
      
       and <code>
        001a000000ABCde
      </code>
      
      ), other separators (<code>
        123-456
      </code>
      
       and <code>
        12-3456
      </code>
      
      )
    </td>
  </tr>
</tbody>
</table>

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](https://docs.oz0.ai/govern/personal-data/associate-people#people-from-triggers).

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](https://docs.oz0.ai/agents/colleagues), and a [chat's](https://docs.oz0.ai/agents/conversations/access-and-history#chats-yours-private-by-default) title and preview.
- The edge holds the keys, sealed with the cluster's master key, or its secrets key where [the roles run apart](https://docs.oz0.ai/fleet/separate-roles). 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:

```bash [Terminal]
oz0 run acme-orders "Refund order 1182" --data-subject alice@example.com --data-subject bob@example.com
```

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:

```bash [Terminal]
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
```

<table>
<thead>
  <tr>
    <th>
      Kind
    </th>
    
    <th>
      Name
    </th>
    
    <th>
      What the rule reads
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        webhook
      </code>
    </td>
    
    <td>
      the flow
    </td>
    
    <td>
      <code>
        payload
      </code>
      
      : the request's JSON body
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        event
      </code>
    </td>
    
    <td>
      the event source
    </td>
    
    <td>
      <code>
        payload
      </code>
      
      : <code>
        {body, event}
      </code>
      
      , 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 <code>
        /body
      </code>
      
       names the same people for the delivery and for every job its events start
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        routine
      </code>
    </td>
    
    <td>
      the routine
    </td>
    
    <td>
      <code>
        payload
      </code>
      
      : <code>
        {input, body}
      </code>
      
      , the job's input and what fired it; a fire on the routine's schedule has its <code>
        input
      </code>
      
       (the routine's own) and an empty <code>
        body
      </code>
      
      , and gets its people when it starts to run, before its first step
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        channel
      </code>
    </td>
    
    <td>
      the binding
    </td>
    
    <td>
      <code>
        payload
      </code>
      
      : <code>
        {text, room, workspace}
      </code>
      
      ; <code>
        identity
      </code>
      
      : the author's <code>
        {id, name, account}
      </code>
    </td>
  </tr>
</tbody>
</table>

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](https://docs.oz0.ai/reference/agent-definition)). A start may name one of them:

```bash [Terminal]
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.

```bash [Terminal]
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 (at `plugin lint`, at save, and again when it fires).
- The job carries its purpose: in its memo, `Job.purpose`, its `run.start` audit 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](https://docs.oz0.ai/govern/inventory#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](https://docs.oz0.ai/govern/reviewed-changes#four-eyes-for-governance-changes) 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.<br />

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.


## Sitemap

See the full [sitemap](https://docs.oz0.ai/sitemap.md) for all pages.
