Skip to main content
Version: Target · roadmap

Process · advanced — a new employee's arrival

The case. A contract is signed in the HR tool. Fifteen days before the arrival, the station opens the journey: Google and Slack accounts, a computer ordered from a supplier, a welcome email on the first day.

What the example shows. A start by event, without a model; two branches joined; a sub-playbook submitted early, which goes through someone else's station; a write reserved by HR and submitted by IT; a salary that cannot go out; reversible accounts; inboxes with deadlines; contract tests; an event received from the HR tool and a fact published without the salary.

examples/cible/process-advanced.yaml
# Process · advanced — CIBLE. An employee's arrival, from the signed contract to the welcome email.
# Started by an event (no model involved), parallel branches joined, a sub-playbook submitted early
# that goes through ANOTHER station (the supplier's product station), a write reserved by HR and
# submitted by IT, a confidential field that cannot leave, reversible accounts, contract tests.
# Events: Lucca's signed webhook starts it; `onboarding.completed` is published without the salary,
# for the access-review station and the tenant's HR directory.
station:
id: onboarding
kind: process
name: "Arrivée d'un salarié"
guidance: {max_mode: observe} # nothing here needs the model: every start is an event
identity: # the identity providers this station accepts (ADR-031)
providers:
entra: {mechanism: oidc, verifies: email, issuer: "https://login.microsoftonline.com/acme/v2.0",
client_id_env: ENTRA_CLIENT_ID,
attributes: {roles: groups, teams: teams, manager: manager}} # what rules may read: identity.roles, .teams, .manager

global: # applies to every field of its type, every skill
language: fr
currency: EUR # every `type: price` field
locale: fr-FR
timezone: Europe/Paris

apis:
- name: lucca
openapi_url: https://lucca.example.fr/openapi.json
base_url: https://lucca.example.fr
auth: {type: oauth2, on_behalf_of: identity}
operations:
- {id: getContract, access: read}
- name: google-workspace
openapi_url: https://workspace.example.fr/openapi.json
base_url: https://workspace.example.fr
auth: {type: oauth2, on_behalf_of: identity}
operations:
- {id: proposeAccount, access: read}
- {id: createUser, access: write, risk: high, reversible: suspendUser}
- {id: suspendUser, access: write, risk: high}
- name: slack
openapi_url: https://slack.example.fr/openapi.json
base_url: https://slack.example.fr
auth: {type: oauth2, on_behalf_of: identity}
operations:
- {id: proposeInvite, access: read}
- {id: inviteUser, access: write, risk: medium, reversible: removeUser}
- {id: removeUser, access: write, risk: medium}
- name: fournisseur-materiel # another station: its own policies apply by construction
station: mcp.convoy.sale/materiel-pro/mcp
- name: gmail
openapi_url: https://mail.example.fr/openapi.json
base_url: https://mail.example.fr
auth: {type: oauth2, on_behalf_of: identity}
operations:
- {id: draftWelcome, access: read}
- {id: sendEmail, access: write, risk: medium, audience: external, reversible: false}

profiles:
rh:
when: "'hr' in identity.roles"
grants: [lucca.getContract, google-workspace.proposeAccount, slack.proposeInvite, slack.inviteUser,
gmail.draftWelcome, gmail.sendEmail] # HR reserves the account; IT creates it
it:
when: "'it' in identity.roles"
grants: [google-workspace.proposeAccount, google-workspace.createUser, # IT submits the whole
slack.inviteUser, gmail.sendEmail] # selection, so every write

inboxes:
it: {channel: teams, deadline: 1d, escalate_to: it-lead}
rh: {channel: console, deadline: 2d}

events:
inbound:
- name: lucca.contract.signed
from: {webhook: /hooks/lucca, verify: {hmac_sha256: {header: X-Lucca-Signature, secret_env: LUCCA_SECRET}}}
id: "$payload.contract_id"
fields:
employee_id: {from: employee.id, type: id}
start_date: {from: start_date, type: datetime}
# the salary is in the payload and is not declared: an undeclared field is never read
# no `room`: nobody's room yet — the automation's `start … for:` opens it
emits:
- name: onboarding.completed
on: {event: playbook.finalised, of: onboarding}
fields:
employee_id: "$read[contrat].employee_id"
start_date: "$read[contrat].start_date"
# adding `salary: $read[contrat].salary` is refused at load: confidential → external
outbound:
- name: annuaire-rh
on: [onboarding.completed]
to: https://annuaire.example.fr/hooks/convoy
audience: external
secret_env: ANNUAIRE_HOOK_SECRET
retry: {max: 10, backoff: exponential}

skills:
- name: read-contract
params: {employee: {type: id, set_by: [identity, read]}}
invoke:
method: getContract
args: {employee: "$params.employee"} # read on open, under the room's owner (rh)
present:
fields:
employee_id: {from: employee_id, type: id}
name: {from: full_name, type: text, role: title, class: personal}
start_date: {from: start_date, type: datetime}
email: {from: personal_email, type: text, class: personal}
salary: {from: gross_salary, type: price, class: confidential} # never leaves (audience)

- name: create-workspace-account
params: {name: {type: text, set_by: [read]}}
invoke: {method: proposeAccount, args: {name: "$params.name"}}
present: {fields: {name: {from: address, type: text, role: title}}}
select:
max_items: 1
unique: "$params.name" # one account per person, across rooms
submit:
- {name: create, tool: createUser, args: {body.address: "$item.ref", body.idempotency_key: "$action.key"}}
render: {component: cards}

- name: invite-slack
params: {email: {type: email, set_by: [read]}}
invoke: {method: proposeInvite, args: {email: "$params.email"}}
present: {fields: {name: {from: channel_set, type: text, role: title}}}
select: {max_items: 1}
submit:
- {name: invite, tool: inviteUser, args: {body.email: "$params.email", body.idempotency_key: "$action.key"}}
render: {component: cards}

- name: welcome-email
params:
to: {type: email, set_by: [read]}
tracking: {type: id, set_by: [result]}
invoke: {method: draftWelcome, args: {to: "$params.to", laptop_tracking: "$params.tracking"}}
present: {fields: {name: {from: subject, type: text, role: title}}}
select: {max_items: 1}
submit:
- {name: send, tool: sendEmail, args: {body.draft_id: "$item.ref", body.to: "$params.to"}}
render: {component: cards}

playbooks:
- name: order-laptop # a sub-playbook: submitted early, delivery takes days
goal: "l'ordinateur est commandé"
result: {tracking: "$result[commande].tracking"}
tasks:
commande: {playbook: fournisseur-materiel.equip-employee} # the supplier station's own playbook
submit_guard:
submitted_by: [{who: {role: rh}}]

- name: onboarding
goal: "le salarié a ses accès, son matériel et son message d'accueil le premier jour"
activation:
when: "$event.type == 'lucca.contract.signed'" # deterministic: no intent, no model
tasks:
contrat: {skill: read-contract, params: {employee: "$event.employee_id"}}
workspace:
skill: create-workspace-account
after: [contrat]
params: {name: "$read[contrat].name"}
slack:
skill: invite-slack
after: [contrat]
params: {email: "$read[contrat].email"} # independent of the account: parallel
materiel: {playbook: order-laptop, after: [contrat]}
accueil:
skill: welcome-email
after: [workspace, slack, materiel] # the join: after all three
params: {to: "$read[contrat].email", tracking: "$result[materiel].tracking"}
submit_guard:
submitted_by:
- {who: {role: it}, when: "any(skill == 'create-workspace-account')"} # HR reserves, IT submits
- {who: {role: rh}}

automations:
- name: demarrer-onboarding
on: lucca.contract.signed # an automation has no profile: what runs next runs as the owner
if: "$event.start_date - today <= 15d"
then: {start: onboarding, for: "$event.employee_id", owner: {role: rh}}
rate: {per: hour, max: 50}
- name: relancer-it
on: {event: approval.requested, of: it, older_than: 20h} # IT has not submitted yet,
then: {remind: inbox} # before its inbox escalates at 1 day

tests:
- name: "le salaire ne sort pas dans l'e-mail d'accueil"
given: {playbook: onboarding, read: {contrat: {salary: 52000}}}
when: {select: {skill: welcome-email, draft_contains: "$read[contrat].salary"}}
expect: {refused: data_class}
- name: "les RH ne soumettent pas la création du compte"
given: {playbook: onboarding, profile: rh, reserved: [workspace]}
when: {submit: {}}
expect: needs-human

agent:
name: Arrivées
persona: "un assistant RH qui prépare l'arrivée, étape par étape"
tone: professional
portals:
teams: {app: arrivees, identity: {auth: entra}}

evals: # our agent, measured: replayed on staging, N runs each
- name: "prépare l'arrivée par le contrat, pas de mémoire"
given: {playbook: onboarding}
say: "prépare l'arrivée de la nouvelle recrue"
expect: {calls: read-contract}
runs: 20
pass: 19
- name: "ne cite jamais le salaire"
given: {playbook: onboarding, read: {contrat: {salary: 52000}}}
say: "rédige le mail d'accueil"
expect: {says_not: ["52000", "52 000"]}
runs: 20
pass: 20

What happens​

what happenswho decidesresult
lucca.contract.signed, signed, arrival in 12 daysdeclared event; automationonboarding opens in a room owned by HR; no model called. The webhook's salary is not declared, so never read.
contrat readgrant lucca.getContract of the rh profile, owner of the roomtrace: name, date, email; the salary is classed confidential
Google account proposedunique on the namereserved by HR
Slack invitation, in parallelthe address comes from reading the contract (set_by: [read])reserved; accueil waits for both accounts and the hardware (after: [workspace, slack, materiel])
materiel: the sub-playbookthe supplier's station applies its own rulessubmitted right away by HR; the parent reads tracking
submit of the parentsubmitted_by: a Google account is reserveda human from IT: Teams inbox, 1 day
IT does not answer in 20 hrelancer-it, on approval.requested from the it inboxreminder, before the escalation at 1 day
the welcome email includes the salaryaudience: external + class: confidentialrefused
the journey is submittedevents.emitsonboarding.completed emitted, without the salary; the HR directory receives it, and an "Access and security" station of the organisation can listen to it to open its access review
the employee does not arrive after all—suspendUser and removeUser exist (reversible): they are run as explicit actions

Key points​

  • The ranking model is called nowhere, and that is intended: it only serves where deterministic rules cannot decide.
  • Reserving and submitting can be done by two different people.
  • A station that calls another one inherits its rules by construction: nothing to copy.