Skip to main content
HUMΛN
Developer
Developer

Building human-in-the-loop workflows with the HUMΛN Workforce Module

HUMΛN Team··25 min·Technical (Developers)

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_idteam_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:

  1. Check org routing config for this renderer_id
  2. Fall back to manifest routing_hints.default_route_type
  3. 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_id against 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