Building human-in-the-loop workflows with the HUMΛN Workforce Module
Every agent workflow eventually hits the same wall.
The model has done its job. It has judged, synthesized, drafted, scored. Now a human needs to decide: act on it, revise it, reject it, escalate it. And this is where most teams make the same three mistakes: they build a notification that fires an email, they put a yes/no Slack button in the channel, and they hardcode the routing to whoever is available this week.
Six months later they have fragmented queues across three tools, a yes/no UX that tells the human nothing about what they are actually deciding, and a routing config that requires a code deploy to change.
The HUMΛN Workforce Module is a platform primitive that solves this properly — once. Any bundle declares rich work item renderers in its manifest. The Workforce Cloud shell is the universal inbox. Routing is configuration, not architecture. Every action creates an immutable provenance record.
This article walks through the full journey: from declaring your first renderer to seeing a live work item in the inbox.
The mental model
Before the Workforce Module, HUMΛN bundles could declare two extension surfaces:
cp_extension→ Control Plane shell. Admin dashboards, metrics, policy gates. The operator surface.companion_module→ Companion shell. Mode pickers, sidebar panels, thread tools. The user surface.
The Workforce Module adds the third:
workforce_module→ Workforce Cloud shell. Work item inbox, renderer types, routing config. The human-doing-the-work surface.
The mental model for workforce_module is straightforward:
Renderer type = declared intent. Your manifest says how the human should interact with this work item. An approval_gate shows a confidence bar and two buttons. A review_artifact shows the full content inline with a comment field. An edit_artifact opens an inline editor with diff view. A guided_workflow shows a multi-step attestation form. The CP shell picks the right component — you declare the intent.
Routing target = configuration. Your manifest declares a hint (default_route_type: team). The org admin overrides it in CP settings after install. If no config exists, the item falls back to the caller's personal queue. Your ctx.approval.request() call never changes when routing changes.
Every action is auditable. Status transitions, action taken, who, when, what note — all written to the provenance record. The audit trail is not an afterthought.
Three minutes: scaffold to live manifest
$ human extension create my-signal-review --type workforce
Interactive flow:
✦ HUMΛN Extension Scaffold — Workforce Module
How many work item renderers? 1
─── Renderer 1 of 1 ───────────────────────────────────────
Renderer id: signal_review
Label: Signal Review
Renderer type:
❯ approval_gate — artifact + evidence + go/no-go
review_artifact — full artifact inline + comment + actions
edit_artifact — inline editor + diff + save as revised
guided_workflow — multi-step form + attestation fields
Artifact kinds (comma-separated): signals.executive_brief, signals.product_gap_memo
Default route type:
❯ team
personal
Escalate after (hours): 48
Emit feedback on action? (Y/n): Y
✓ Generated:
manifest.yaml (workforce_module block populated)
src/renderers/signal_review.ts (typed renderer config + JSDoc)
src/ctx-usage-example.ts (ctx.approval.request() calls)
tests/__tests__/work-items.test.ts (WorkItemTestHarness fixtures)
README.md (routing config guide)
Non-interactive (CI / automation):
human extension create my-signal-review \
--type workforce \
--renderer-type approval_gate \
--artifact-kinds signals.executive_brief,signals.product_gap_memo \
--actions approve:release_to_delivery,reject:dismiss_with_feedback,escalate:re_route \
--route-type team \
--escalation-hours 48 \
--feedback-emit
Validate it immediately:
$ human extension validate manifest.yaml
✓ Manifest valid
✓ workforce_module: 1 renderer declared
✓ signal_review: approval_gate, 3 actions, feedback_emit: true
Preview your renderer in a local inbox (no full stack needed):
$ human extension dev
✦ HUMΛN Extension Dev Server
Workforce inbox: http://localhost:4001/workforce
Renderer preview: http://localhost:4001/workforce/preview/signal_review
Creating mock work item for signal_review...
→ Mock artifact: signals.executive_brief (auto-generated)
→ Work item routed to: personal (local dev fallback)
Manifest anatomy
The generated manifest.yaml looks like this:
kind: humanos.extension.v1
id: "org.your-org.my-signal-review"
name: "My Signal Review"
version: "0.1.0"
publisher: your-org
trust_tier: community
workforce_module:
work_item_renderers:
- id: signal_review
label: "Signal Review"
description: "Review a judged market signal before delivery"
renderer_type: approval_gate # ← declared intent
artifact_kinds:
- signals.executive_brief
- signals.product_gap_memo
routing_hints:
default_route_type: team # ← routing hint (org config overrides)
urgency_default: normal
escalation_policy:
escalation_after_hours: 48
escalation_route: personal # fallback if team doesn't act
actions:
- id: approve
label: "Act on this signal"
style: primary
effect: release_to_delivery
- id: reject
label: "Not relevant"
style: danger
effect: dismiss_with_feedback
requires_note: true # note required before reject fires
- id: escalate
label: "Route to team"
style: secondary
effect: re_route
feedback_emit: true # every action → learning loop
Key fields explained:
| Field | What it does |
|---|---|
id |
Stable wire key — must be unique within this manifest's work_item_renderers[] |
renderer_type |
Determines which CP component renders this item (see type guide below) |
artifact_kinds |
Declares what artifact types this renderer expects; used for type checking at route time |
routing_hints.default_route_type |
personal or team — org admin can override in CP settings |
routing_hints.escalation_policy |
What happens if the item sits unactioned for N hours |
actions[].effect |
Platform effect that executes server-side when the action fires |
actions[].requires_note |
When true, the CP interface forces the human to type a note before the action executes |
feedback_emit |
When true, every action creates a learning feedback signal in the HumanOS learning loop |
Common validation mistakes:
❌ renderer_type: "custom_renderer" is not registered.
Available: approval_gate, review_artifact, edit_artifact, guided_workflow
❌ actions[1].effect: "approve_and_send" is not a known ActionEffect.
Common effects: release_to_delivery, dismiss_with_feedback,
return_to_worker_with_notes, re_route, trigger_brand_voice_pipeline,
create_revised_artifact, add_to_watched_sources, mark_ready_for_publish,
create_compliance_record
→ Custom effects are allowed but must be documented for your action handlers.
Renderer type selection guide
Four types cover virtually every human-in-the-loop pattern:
approval_gate
When: The human needs context to make a go/no-go decision. Artifact preview, confidence score, evidence chain. Two to four action buttons.
Signals uses it for: signal_review — confidence bar + evidence links + approve/reject/escalate.
Brand Voice uses it for: qa_sign_off — post-QA artifact preview + sign-off / flag issues.
review_artifact
When: The human needs to read the full artifact before deciding. Full content inline, read-only, optional comment field, action buttons.
Signals uses it for: content_brief_approval — full brief inline before sending to brand voice pipeline.
Brand Voice uses it for: content_draft_approval — full draft inline before publishing pipeline.
edit_artifact
When: The human needs to make changes rather than approve or reject. Inline editor with diff view against the original. "Save edits" creates a new revised artifact with provenance.
Signals uses it for: prd_draft_review — PRD draft editing before product team sign-off.
guided_workflow
When: The work item requires structured input, not freeform comment. Multi-step form with attestation fields that create a logged compliance record.
Signals uses it for: compliance_acknowledgment — regulatory signal review with formal attestation.
Actions and effects
Every action declares an effect — the server-side operation that fires when the human clicks. Effects determine the new work item status and what downstream happens.
Built-in effects:
| Effect | Resulting status | Use case |
|---|---|---|
release_to_delivery |
completed | Send artifact to next step in workflow |
dismiss_with_feedback |
rejected | Reject with optional feedback signal |
return_to_worker_with_notes |
in_review | Send back with a note for revision |
re_route |
pending | Escalate or route to a different target |
trigger_brand_voice_pipeline |
completed | Fire the brand voice writing pipeline |
create_revised_artifact |
completed | Save edited content as a new artifact version |
add_to_watched_sources |
completed | Add a URL/source to the signals watch list |
mark_ready_for_publish |
completed | Mark artifact as cleared for publishing |
create_compliance_record |
completed | Write a formal compliance attestation record |
Custom effects are allowed — declare any string as an effect. The platform logs it and transitions the status. Your workflow step receives the effect string and can branch on it.
feedback_emit: true means every action execution emits a learning feedback signal with a signal_strength derived from the effect: explicit approvals emit explicit_positive, dismissals emit explicit_negative, revision requests emit correction, escalations emit escalation. This feeds the HumanOS learning loop.
requires_note: true blocks the action button until the human types a note. The note is stored in actions_taken and visible in the audit trail. Use this for any action that needs context — revision requests, rejections, compliance attestations.
Routing config
Routing is the only piece that requires org admin configuration after install. Everything else works with zero config.
Personal routing (zero config):
If you declare default_route_type: personal, work items go to the calling user's personal queue immediately. No admin setup needed. Best for founders, solo operators, or any workflow where the person who triggers it should also review it.
Team routing (requires org config):
If you declare default_route_type: team, the org admin maps renderer_id → team_id in CP settings:
CP → Workforce → Settings → Routing rules
content_brief_approval → team: marketing
signal_review → team: signals-team
compliance_acknowledgment → team: legal
Resolution algorithm:
- Check org routing config for this
renderer_id - Fall back to manifest
routing_hints.default_route_type - Fall back to personal queue (always works)
Escalation:
If a work item is not actioned within escalation_after_hours, it is re-routed to escalation_route (typically personal). The Companion notification for the caller fires at this point if urgency is high or urgent.
Testing with WorkItemTestHarness
WorkItemTestHarness from @human/platform-extensions/testing lets you test the full work item lifecycle — create, action, assert effect, assert feedback, assert provenance — with no external dependencies, no database, no API server.
Full test lifecycle:
import { describe, it } from 'vitest';
import { WorkItemTestHarness, MockArtifacts } from '@human/platform-extensions/testing';
import { signalReviewRenderer } from '../src/renderers/signal_review.js';
describe('signal_review renderer', () => {
it('releases to delivery on approve', async () => {
const harness = new WorkItemTestHarness({
renderer: signalReviewRenderer,
mockArtifact: MockArtifacts.executiveBrief(),
routeTo: 'personal',
});
await harness.executeAction('approve');
harness.assertEffect('release_to_delivery');
harness.assertWorkItemStatus('completed');
harness.assertFeedbackEmitted({ signal_strength: 'explicit_positive' });
harness.assertProvenanceAnchored();
});
it('requires a note for reject', async () => {
const harness = new WorkItemTestHarness({ renderer: signalReviewRenderer });
// This throws — requires_note: true
await expect(harness.executeAction('reject')).rejects.toThrow('requires a non-empty note');
// With a note, it succeeds
await harness.executeAction('reject', { note: 'Covered by existing analysis' });
harness.assertEffect('dismiss_with_feedback');
harness.assertWorkItemStatus('rejected');
});
it('cannot re-use a completed harness', async () => {
const harness = new WorkItemTestHarness({ renderer: signalReviewRenderer });
await harness.executeAction('approve');
await expect(harness.executeAction('reject')).rejects.toThrow('already completed');
});
});
Guided workflow attestation:
describe('compliance_acknowledgment renderer', () => {
it('logs compliance record on acknowledge', async () => {
const harness = new WorkItemTestHarness({
renderer: complianceAcknowledgmentRenderer,
mockArtifact: MockArtifacts.custom('signals.compliance_signal'),
});
await harness.executeAction('acknowledge', { note: 'Reviewed with legal. Action: update privacy policy.' });
harness.assertEffect('create_compliance_record');
harness.assertWorkItemStatus('completed');
harness.assertActionExecuted('acknowledge');
// compliance_acknowledgment has feedback_emit: false
expect(harness.feedbackHistory.length).toBe(0);
});
});
CI integration — run validateWorkforceModule() in pre-commit or build:
import { validateWorkforceModule } from '@human/platform-extensions';
import { parse } from 'yaml';
import { readFileSync } from 'fs';
const manifest = parse(readFileSync('manifest.yaml', 'utf8'));
const result = validateWorkforceModule(manifest.workforce_module);
if (!result.valid) {
result.errors.forEach((e) =>
console.error(`${e.field}: ${e.message}\n → ${e.remediation}`)
);
process.exit(1);
}
What the human sees
When ctx.approval.request() fires in a workflow, a work item row is created in workforce_work_items and the routing target is resolved. Here is what happens from the human's perspective:
1. Companion notification (urgency-gated)
If urgency is high or urgent, the "Things waiting for you" section in the Companion sidebar updates immediately. The item shows the renderer label, urgency badge, and time. For approval_gate items, inline approve/reject buttons appear so the human can act without leaving Companion.
2. CP inbox
/workforce/inbox shows all pending items for the user — personal queue and team queues. Each row shows renderer type chip, renderer label, urgency, and time. Click "Open" to go to the work item detail.
3. Work item detail (/workforce/inbox/[workItemId])
The renderer type registry resolves the manifest declaration and renders:
approval_gate: artifact preview block (title, summary line, confidence bar), evidence links, action buttons.review_artifact: full artifact body inline, read-only, note field, action buttons.edit_artifact: artifact content with "edit mode" note (full inline editor ships in a later release; the artifact inspector handles edits today).guided_workflow: step-by-step form with attestation fields and note field.
4. Action execution
The human clicks an action button. If requires_note: true, the note field must be non-empty. The action POSTs to /v1/workforce/work-items/:id/action, which:
- Validates
action_idagainst the manifest - Executes the effect (status transition, downstream trigger)
- Emits feedback if
feedback_emit: true - Writes a provenance anchor: who, when, what action, what effect, what note
5. History
Completed/rejected items remain in the inbox with terminal status. The full actions_taken array is visible in the work item detail. Every action is auditable.
Wiring ctx.approval in your workflow
Once your manifest is installed and the org routing config is set, the workflow code is three lines:
import type { ExecutionContext } from '@human/agent-sdk';
export async function runSignalsWorkflow(ctx: ExecutionContext, signalCluster: SignalCluster) {
// 1. Generate the artifact
const brief = await ctx.artifacts.create({
artifact_type: 'signals.executive_brief',
content: await generateBrief(ctx, signalCluster),
});
// 2. Route to human for review
const { work_item_id } = await ctx.approval.request({
installation_id: ctx.identity.org.installationId, // from env or context
renderer_id: 'signal_review',
artifact_id: brief.artifact_id,
urgency: signalCluster.confidence > 0.9 ? 'high' : 'normal',
});
// 3. Wait for the human (timeout: 48h)
const result = await ctx.approval.awaitResult(work_item_id, {
timeoutMs: 172_800_000,
});
if (result.status === 'completed') {
// Human approved — proceed downstream
await ctx.events.emit('signal.approved', { brief_id: brief.artifact_id });
} else {
// Human rejected or cancelled — log and stop
await ctx.events.emit('signal.rejected', { brief_id: brief.artifact_id });
}
}
The routing target is resolved server-side from org config. The workflow code is identical whether routing goes to personal, team, or (in future) inter-org. Routing is config, not architecture.
North star: what's coming
V1 ships personal + team routing. The routing model was designed to extend cleanly:
Inter-org routing — Route work items to a team in a partner org. Requires an invitation/acceptance protocol and cross-org delegation. The manifest declaration (routed_to_type: inter_org) is already in the schema; the platform returns a 400 today with a clear NotImplementedForMVP message.
Public cloud routing — Capability-matched Passport routing to external verified humans. A compliance acknowledgment that requires a certified auditor. A legal review that routes to a qualified solicitor. The routed_to_type: public path is stubbed the same way.
What stays the same when routing gets more powerful: your manifest. The renderer_type, actions, and effects you declare today will work unchanged when inter-org and public cloud routing ship. Your ctx.approval.request() call will not change. Only the org routing config and the platform's routing resolution logic evolve.
The manifest is the contract. The routing is the config.
Reference implementations
Both reference implementations are live and can be used as annotated examples:
-
Brand Voice Suite (
manifests/bundles/human-bundle-brand-voice.yaml) — Example One. Three renderers:content_draft_approval(review_artifact),qa_sign_off(approval_gate),publish_approval(approval_gate). The approval workflow for every piece of content the brand voice pipeline produces. -
Signals (
manifests/bundles/human-bundle-signals.yaml) — Example Two. Five renderers:signal_review(approval_gate),content_brief_approval(review_artifact),prd_draft_review(edit_artifact),compliance_acknowledgment(guided_workflow),watch_candidate_approval(approval_gate). All four renderer types in one bundle.
Workforce Module: kb/162_platform_workforce_module.md | SDK: @human/platform-extensions | CLI: human extension create --type workforce