Decision
One global hop executor. One work runner per module. One composition coordinator. Chosen shape is that split, drawn against the rejected shape. Why the split is why a third module is a definition, a work runner, and a link. Endings and custom pathways and Conditionals say where a continue goes. This page is the Slice 0 contract. There is one module type and one hop compile. It does not cut over runtime, invert coordinator handoff, restyle Admin → Modules, or change the Modules Demo walkthrough. Those wait on later internals tickets and TEN-1333.Chosen shape
One global hop executor applies every hop. One work runner per module does that module’s work. One composition coordinator reads the job’s compiled links and activates the next instance, or Stops. The rejected shape is a Module class that owns a private stage orchestrator and starts the next module. The class carries its own hop table and names the module that runs next. The diagrams on this page are the contract. Runtime still applies a hop throughTenzoStageOrchestrator (TEN-1316). Resume Review continue_handoff still activates Interview (TEN-1315).
Why the split
A hop is one operation. The compiled plan names from-stage, to-stage, and action profile. The hop executor writes the stage, records history, and runs that plan’s actions. The operation is the same for every module. Work differs per module. Resume Review scores a resume. AI Interview runs outreach. That procedure stays in the module’s work runner. A third module (Scheduling, ID Verification, or a test double) is a definition, a work runner, and a link. The definition lists sit-able stages, inner transitions, and outputs. The work runner reportscontinue(output_id) or hold. The link names the next instance or Stop. The module does not copy the hop table and does not name the next module. Adding a module is that boundary.
A pass
On a pass, the work runner reportscontinue(output_id). The coordinator follows the job’s compiled link for that output. The next work runner activates. The one hop executor writes the stage.
Hold does not follow a link. The candidate stays with that work runner. A missing link also leaves the candidate in place (Endings and custom pathways).
Endings and custom pathways
Endings and custom pathways are composition data. The work runner reportscontinue(output_id) or hold. The coordinator reads the job’s compiled link for that output: the next instance, or Stop.
A missing link leaves the candidate in place.
Changing an ending edits the link. The work runner, the hop executor, and the module definition stay as they are.
Two jobs can share one output and end differently. One job’s link starts the next module. The other job’s link Stops. Same output, different endings draws that, and a conditional beside it.
Conditionals
A conditional is composition data on the same coordinator. Either the work runner reports a richer output id, and each id has its own link, or the link carries a predicate and the one coordinator matches it. The conditional is not code inside the module, and it is not a second hop list. The recruiter editor and the predicate matcher are not runtime yet. This page does not add either one. The named follow-ons remain TEN-1315, TEN-1316, and TEN-1333.Same output, different endings
The top is two jobs and one output. The bottom is a conditional: two links from richer output ids, or a predicate on the link. The recruiter editor and the predicate matcher are not runtime yet (Conditionals).Who owns what
Sit-able stages are the module stages on the compiled
ModuleDefinition. The module-run FSM (pending / evaluating / …) is lease and work progress, not a second stage language. Recruiter-visible names on the click-in stay as they are until a UI ticket.
Outputs are keys on the definition (passed, resume_rejected). They do not name the next module. Composition links map an output to a module instance start or to Stop.
Adding a module
The boundary is Why the split. May change: one module definition, that module’s work handlers, tests, composition links. Must not change: the global hop executor, the pipeline compiler, the composition coordinator. The hop executor may record which module owns a hop for history. It must notswitch on module identity. The coordinator must not grow a START_<MODULE> action. It activates the instance the composition link names, or it Stops.
Today’s JobWorkflowCoordinator.START_AI_INTERVIEW is the legacy V1 adapter. TEN-1315 replaces it with this contract’s activate / Stop / hold / close / no-op.
Layers
TenzoStageOrchestrator. TEN-1316 points that executor at compiled plans. Extra actions on the click-in still do not persist or run.
Handoff rules
- A module reports continue or hold. Continue carries an
output_idonly. The module does not report close / Stop. - The coordinator applies global gates (canceled, terminal application decision, global hold) before resolving a hop.
- Continue always goes through the compiled composition index (
CompiledPipeline.output_destinations). First matching link wins: activate that instance, or Stop. - If none match, the candidate stays. Outreach is not invented.
- Authored closing outputs (
resume_rejected,fraud_rejected) are ordinary outputs. Composition can Stop or send them onward; the work runner cannot skip that lookup. - Resume Review omitted Interview means the continuing resume output links to Stop. No pending call.
- Modules do not call the next module’s activate. Resume Review
continue_handoffstill does that today; TEN-1315 inverts it. - Hops resolve through
TransitionLookupKey(from,to,action_profile). Same stage pair with a different profile is a different compiled plan. TEN-1316 executes that plan; it does not take a private action list.
Canonical types
The compiler already accepts a third definition without product branches. The contract resolver does the same for handoff.
Shared skeleton
Resume Review and AI Interview implement one activate, claim, transition, and terminal-result shape. A further module implements that shape and is registered by its module id. The composition coordinator does not gain a start action for it, andJobWorkflowCoordinator is unchanged.
A terminal result is continue, with an output id, or hold. Continue does not name the next module. In-progress work is not a terminal. Interview reports continue only when its run is completed and the candidate sits on one of that module’s output stages. A passed or bypassed Resume Review stays a pass if a later stage is a rejection output.
Transition asks the existing Tenzo stage writer to apply the hop. It does not add pairs and does not filter pairs. Who can move where stays on that writer.
Candidate position
A candidate’s place is the module instance they are in, the sit-able stage they sit on, and any overlay. The instance id is not the module kind, so the same kind can appear twice. The recruiter-facing stage is derived from that place: an overlay hides the sit-able stage, and with no overlay the sit-able stage is the view. A local move is decided from the sit-able stage on that instance. The derived recruiter stage is not an input. Overlay enter and release are not local moves. The decision only accepts a transition the module definition already has between two of its sit-able stages. Applying a compatibility hop is still the existing stage writer, which keeps today’s allowed moves.Out of scope here
- Cutover, snapshot of existing jobs, Progress to Interview as composition data
- Admin Modules chrome, extra-action picker, Modules Demo walkthrough (TEN-1333)