Human in the Loop
A pause button you can wire into any flow: hitl suspends execution and waits for a real person — a circle member or an external respondent — to approve, reject, or fill in a form before the run continues, with the interaction UI rendered by a bonded View.
Working with it
Opening a Human in the Loop launches an agent chat — its dedicated working surface.
How it appears
The same element type rendered as a definition, a circle instance, and a live workspace card.
When to use / not
When to use
- Gating a consequential action behind human sign-off — a deployment, an expense, a publish — so the flow only proceeds once someone approves.
- Collecting structured input from a person mid-flow via a bonded View's form, then resuming with their answers attached to the output.
- Fanning an approval out to several people with a quorum policy (any one, a count, a percentage, or all) — including external respondents reached by per-person link, email, slack, or sms.
- Holding a run open for hours or days while a human decides, with a timeout fallback that fails, escalates, or auto-resolves on silence.
When not to use
- Fully automated decisions with no person in the loop — encode the rule in an automation condition or a code action (python, javascript) instead.
- Conversational, multi-turn agent chat — that is a lab / agent element; hitl is a single explicit submit, not a dialogue.
- Routing notifications without blocking the flow — if you only need to tell someone something, send via the io channel element (slack, email) and keep going.
Topology
Created from the library and placed inside an app or circle. It is a top-level building block you compose with other elements.
Properties
promptstring- Default prompt or instruction displayed to respondents
viewstring- View element (slug or UUID) whose template renders the approval/response UI. Bonds this HiTL to a View for respond_view rendering.
audienceobject- Pre-defined respondents. Can be overridden or supplemented at invoke time.
quorumobject- How many responses are required to complete the action
timeoutobject- What happens if quorum is not reached in time
Capabilities
Defined for this element
- Queue
- Observe
- Network
Operations
- activityGET
- approvePOST
- attachPOST
- attachmentsGET
- batch_statsGET
- composePOST
- contextGET
- createPOST
- deleteDELETE
- detachPOST
- diagnosticsGET
- disablePOST
- enablePOST
- export_bundleGET
- getGET
- get_attached_modifiersGET
- import_bundlePOST
- intentionGET
- invokePOST
- list_attachmentsGET
- logsGET
- pendingGET
- promotePOST
- readmeGET
- readme_updatePOST
- rejectPOST
- remove-modifierPOST
- respondPOST
- respond_viewGET
- restorePOST
- run_cancelPOST
- run_getGET
- runsGET
- schemaGET
- sourceGET
- source_branchesGET
- source_promotePOST
- source_repairPOST
- source_statusGET
- source_validatePOST
- statsGET
- submitPOST
- treeGET
- updatePATCH
- update_metaPATCH
- versionGET
Ports
Inputs
- requestrequest
- responserequest
Composition
Errors / when it fails
- A View element must be bonded to render the human interaction UI
- Define at least one member or external in audience, or pass audience at invoke time
- Set quorum.value when mode is count or percentage
Validation rules
- Long timeout (>7 days) — ensure escalation is configured
- External respondents configured but no auth-policy attached — consider access control
Human in the Loop (hitl)
Category: actions | Form: | Symbol: Hi
Collect structured human input via a connected View
Platform-native action for structured human interaction. Define input_schema (what the human sees) and output_schema (what they submit back). Always requires a View element bonded to it — the View renders the interaction UI. When invoked, generates unique links for each audience member (circle members or externals). Supports fan-out to multiple respondents with configurable quorum (count, percentage, or all). Can be wired into automations or attached to agents as a tool. Pre-define audience on the element or pass dynamically at invoke time.
Guide
Pause execution for human approval or input
What It Does
Human in the Loop (HITL) suspends a running flow and waits for a designated person or role to make a decision before execution continues. It presents the approver with a prompt, optional context from previous flow steps, and configurable response options. When the human responds, the flow resumes with the approval decision attached to the output. HITL supports timeouts with configurable fallback behavior, multi-channel notifications, and role-based assignment.
Element Definition
| Property | Value |
|---|---|
| Type | hitl |
| Category | apps |
| Form | atom |
| Symbol | person_check / #EC4899 |
Properties
| Field | Type | Default | Description |
|---|---|---|---|
description | string | — | Description shown to the approver |
prompt | string | — | Question or instruction for the approver (required) |
options | array | See below | Response options. Each item: label, value, color (default, success, warning, danger) |
options default | — | [{label: "Approve", value: "approve", color: "success"}, {label: "Reject", value: "reject", color: "danger"}] | Default approve/reject buttons |
assignees.users | array | — | Specific user references who can respond |
assignees.roles | array | — | Role names — any member of the role can respond |
assignees.policy | string (enum) | any | Approval policy: any (first response wins) or all (all must respond) |
timeout.duration_hours | integer | 24 | Hours before timeout action triggers |
timeout.action | string (enum) | fail | Action on timeout: fail, approve, reject, or escalate |
notification.channels | array | ["email"] | Notification channels: email, slack, sms |
notification.reminder_hours | integer | — | Hours before the deadline to send a reminder |
context.include_inputs | boolean | true | Include the flow’s input data in the approver’s view |
context.include_previous_outputs | boolean | true | Include previous step outputs in the approver’s view |
context.custom_fields | array | — | Custom fields to display. Each item: label, expression (CEL) |
Ports
| Direction | Port | Type | Required | Description |
|---|---|---|---|---|
| Input | request | request | Yes | Approval request with prompt and optional form schema |
| Output | response | request | Yes | Approval response: approved, response_data, approver, approved_at |
Topology
- Lives in:
apps/automation/hitl/repository - Referenced by: projects
- Accepts modifiers:
auth-policy,requirements - Uses resources:
variable
Capabilities
| Capability | Description |
|---|---|
approval-workflow | Human approval workflows |
form-collection | Structured data collection from approvers |
timeout-handling | Configurable timeout behavior |
notifications | Multi-channel notifications |
Quick Start
Creating via API
Create a HITL element inside a project:
POST /api/{circle}/{project}/
Content-Type: application/json
{
"element_type": "hitl",
"slug": "deployment-approval",
"name": "Deployment Approval Gate",
"spec": {
"prompt": "Review the deployment plan and approve or reject.",
"assignees": {
"roles": ["release-manager"],
"policy": "any"
},
"timeout": {
"duration_hours": 4,
"action": "reject"
}
}
}
Basic Usage
Invoke the HITL element to pause the flow and wait for a response:
POST /api/{circle}/{project}/deployment-approval/ops/invoke
Content-Type: application/json
{
"prompt": "Deploy version 2.3.1 to production? Tests passed: 142/142.",
"form_schema": null
}
The call blocks until a human responds or the timeout triggers. The response:
{
"approved": true,
"response_data": {},
"approver": "alice@example.com",
"approved_at": "2026-02-25T14:32:00Z"
}
Project Patterns
How HITL Fits Into Projects
HITL is always a gate element in a flow — it sits between an automated step and a consequential action. The typical pattern is: function computes a plan → HITL presents the plan → human approves or rejects → condition routes to execute-plan or abort-plan.
HITL is asynchronous by nature. Flows that contain a HITL step may be suspended for hours or days. The platform durably persists the suspended flow state and resumes it when the human responds. This means the flow’s runtime timeout must be longer than the HITL’s duration_hours.
In development stage, HITL can be auto-approved for testing by responding via the API. In demo stage, it behaves identically to live but with test assignees. In live stage, real users receive real notifications.
Example Project Spec
# Three-option editorial review with 48-hour deadline
elements:
- element_type: hitl
slug: editorial-review
spec:
prompt: "Review the generated content before publication."
options:
- label: "Publish"
value: "publish"
color: success
- label: "Revise"
value: "revise"
color: warning
- label: "Reject"
value: "reject"
color: danger
assignees:
roles: ["editor"]
policy: any
timeout:
duration_hours: 48
action: reject
notification:
channels: [email, slack]
reminder_hours: 8
context:
include_inputs: true
include_previous_outputs: true
custom_fields:
- label: "Word count"
expression: "steps.generate.output.word_count"
- label: "Target audience"
expression: "inputs.audience"
Common Patterns
Expense Approval with Escalation
A HITL that auto-escalates to a manager if the approver does not respond in time:
POST /api/{circle}/{project}/
{
"element_type": "hitl",
"slug": "expense-approval",
"spec": {
"prompt": "Approve expense report",
"assignees": {
"roles": ["finance-approver"],
"policy": "any"
},
"timeout": {
"duration_hours": 24,
"action": "escalate"
},
"notification": {
"channels": ["email"],
"reminder_hours": 12
}
}
}
Structured Form Input
Collect data from the human rather than just approve/reject:
POST /api/{circle}/{project}/
{
"element_type": "hitl",
"slug": "shipping-details",
"spec": {
"prompt": "Provide shipping details for this order",
"options": [
{"label": "Submit", "value": "submit", "color": "success"},
{"label": "Cancel", "value": "cancel", "color": "danger"}
],
"assignees": {
"roles": ["logistics"]
},
"timeout": {
"duration_hours": 8,
"action": "fail"
}
}
}
When invoked, pass a form_schema in the request to define what data the form collects:
POST /api/{circle}/{project}/shipping-details/ops/invoke
{
"prompt": "Order #12345 needs shipping details before dispatch.",
"form_schema": {
"type": "object",
"properties": {
"carrier": {"type": "string", "enum": ["fedex", "ups", "usps"]},
"tracking_number": {"type": "string"}
},
"required": ["carrier", "tracking_number"]
}
}
Applying Modifiers
| Modifier | Use case |
|---|---|
auth-policy | Restrict which users or roles can see and respond to approval requests |
requirements | Require specific variable resources to be present before the HITL activates |
Common Mistakes
Setting timeout.action: approve on sensitive operations.
Auto-approve on timeout is convenient but dangerous for high-stakes decisions. For financial, security, or compliance workflows, always use fail or escalate so silence results in a safe state.
Not handling HITL_REJECTED in the downstream flow.
The HITL_REJECTED error is thrown when the human explicitly rejects the request. If the flow does not catch and handle this error, the entire flow fails. Use a condition on response.approved after the HITL step to route to a rejection handler.
Forgetting to assign the HITL to someone.
If assignees is empty, no one receives a notification and the approval request sits unnoticed until it times out. Always assign to at least one user or role.
Short flow timeout combined with long HITL timeout.
If the outer flow has a 1-hour timeout but the HITL duration_hours is 24, the flow will be killed before the human has a chance to respond. Set the flow’s overall timeout to exceed the HITL’s deadline plus processing time.
Relationships
- Attaches to: auth-policy
- Uses: view, variable
Capabilities
- structured-input: Typed input/output via connected View
- fan-out: Distribute request to multiple respondents with quorum
- link-generation: Generate unique per-respondent URLs
- timeout-handling: Configurable timeout behavior
- notifications: Multi-channel notifications
Properties
| Property | Type | Default | Description |
|---|---|---|---|
prompt | string | — | Default prompt or instruction displayed to respondents |
view | string | "" | View element (slug or UUID) whose template renders the approval/response UI. Bonds this HiTL to a View for respond_view rendering. |
audience | object | — | Pre-defined respondents. Can be overridden or supplemented at invoke time. |
quorum | object | — | How many responses are required to complete the action |
timeout | object | — | What happens if quorum is not reached in time |
notification | object | — | How respondents are notified of pending requests |
Operations
activity
Get /ops/activity | Auth: Read
Get activity events for this element
Scope depends on element capabilities: individual elements query by element_id, project-form elements with activity-scope-members include member activities, circle-level elements with activity-scope-all query the entire circle. Gracefully returns empty list if activities table is missing (old circles).
approve
Post /ops/runs/{run_id}/approve | Auth: Write
Approve a pending HiTL request
Approve a pending request by run_id. Optionally include a reason for audit trail. The run transitions to completed with approval status. Only authorized reviewers (members with write access) can approve.
attach
Post /ops/attach | Auth: Read
Attach this actor to a target element
Call this ON the modifier/resource element, passing target_id of the actor. The target’s contract.yaml must declare the modifier kind in attaches: or uses:. Priority controls evaluation order for modifiers (lower = first).
attachments
Get /ops/attachments | Auth: Read
List all modifiers and resources attached to this element
Returns both modifiers (policy enforcement) and resources (data injection) with is_modifier flag to distinguish. Items in the generated MODIFIER_TYPES list are modifiers; everything else is a resource. Includes cascade_policy and version pin info.
batch_stats
Get /ops/batch_stats | Auth: Read
Get per-element statistics for all children of this element
Returns per-child stats plus an aggregate. Most meaningful on compound or manifest form elements (repositories, circles, projects); atoms have no children so the result is an empty children array with a zeroed aggregate. Uses efficient GROUP BY SQL. Weighted averages for eval scores.
compose
Post /ops/compose | Auth: Execute
Batch add and remove modifiers on this element in a single call
Declarative composition: add modifiers by ref path (slug or path@version) and remove by attachment ID, all in one atomic call on the target element. Each ‘add’ entry resolves the source element, validates topology, attaches with optional priority and cascade policy. Each ‘remove’ entry deletes the attachment row. Returns a summary of what was added and removed. Example: compose({ add: [{ref: “my-prompt”}, {ref: “rate-limit/api@v2”, priority: 50}], remove: [{attachment_id: “uuid”}] })
context
Get /ops/context | Auth: Read
Get connected elements (graph traversal)
Graph traversal showing all connected elements with their relationship type (contains, contained_by, references, referenced_by, attaches, etc.). Use ?depth=N to control traversal depth (default 1) and ?types=actor,data to filter by element types.
create
Post /ops/create | Auth: Write
Create child element
POST to the parent path — element_type goes in the request body, NOT the URL. Both element_type and slug are required and must be non-empty. Name is derived from slug if omitted. Writes to both Git and PostgreSQL. All elements are stored flat under the circle — no intermediate library wrapper rows.
delete
Delete /ops/delete | Auth: Admin
Delete element (soft delete)
Soft delete — sets state to ‘deleted’ but retains the record. Cannot delete elements that have children (has_no_bond precondition) or active runs. Requires admin auth and confirmation.
detach
Post /ops/detach | Auth: Read
Detach this actor from a target element
diagnostics
Get /ops/diagnostics | Auth: Read
Check isolation service connectivity and health
Returns the status of the isolation executor that this element depends on for code execution. Use this to distinguish between user code errors and infrastructure issues when invoke returns 500. Returns isolation_reachable (bool), worker_available (bool), and last_error if any.
disable
Post /ops/disable | Auth: Admin
Disable element (hides and prevents use)
Idempotent — safe to call on already-disabled elements. Optionally pass a reason string. Disabled elements cannot be invoked or executed. Inverse of enable.
enable
Post /ops/enable | Auth: Admin
Enable element (makes usable and visible)
Idempotent — safe to call on already-enabled elements. Transitions element to ready/enabled state. Cannot enable deleted elements. Inverse of disable.
export_bundle
Get /ops/export/bundle | Auth: Read
Export element as downloadable git bundle
On non-root-namespace elements, returns a binary git bundle. On root-namespace (circle) elements, dispatch hands off to the circle’s own export_bundle op, which returns a multi-element JSON envelope with one base64 bundle per child element — this is intentional, not an error.
get
Get /ops/get | Auth: Read
Get element details
Element is already resolved by the routing layer — this returns the cached element, not a fresh DB query. Use the path /api/{circle}/{slug} to address elements.
get_attached_modifiers
Get /ops/attached | Auth: Read
Get elements that are attached to this action
import_bundle
Post /ops/import/bundle | Auth: Write
Import git bundle into element
Accepts a base64-encoded git bundle in the JSON bundle_base64 field. Use overwrite=true to replace existing elements with same slug (default skips duplicates). Imported elements get new UUIDs. Returns counts of imported/skipped elements and any errors.
intention
Get /ops/intention | Auth: Read
Get element intention with full inheritance chain
Returns three levels: direct (this element’s intention), inherited (from category and root), and resolved (final merged intention). Useful for understanding an element’s purpose in context of its hierarchy.
invoke
Post /ops/invoke | Auth: Execute
Submit a task for human response (protocol alias of submit)
Protocol-level alias of
submit. Audience MUST be defined either at the element level (spec.audience) or passed in this input (input.audience). Submitting without audience in either place returns 400 InvalidInput (HITL_NO_AUDIENCE).
list_attachments
Get /ops/targets | Auth: Read
List all elements this action is attached to
Returns all target elements where this action is currently attached. Shows target_id, target_type, priority, and cascade_policy.
logs
Get /ops/logs | Auth: Read
Get execution logs
pending
Get /ops/pending | Auth: Read
List pending HiTL requests
List all runs awaiting human responses. Returns run details, audience, quorum progress, and per-respondent link URLs. No input required.
promote
Post /ops/promote | Auth: Admin
Promote element configuration to a target environment
Only for manifest-form elements (projects). Environments advance: dev → demo → live. dev→demo requires member+ role, demo→live requires admin. Freezes member versions at promotion time (creates snapshot). Persists environment config to spec.environments.
readme
Get /ops/readme | Auth: Read
Get element README.md content
Reads README.md from the element’s git repository. Returns empty content (not an error) if no README exists. Always returns markdown format.
readme_update
Post /ops/readme_update | Auth: Write
Update element README.md content
Creates or overwrites README.md in the element’s git repo. Commits to the draft branch. Content must be provided as a markdown string.
reject
Post /ops/runs/{run_id}/reject | Auth: Write
Reject a pending HiTL request
Reject a pending request by run_id. A reason should be provided for audit trail and to inform the requester why their request was denied. The run transitions to failed with rejection status.
remove-modifier
Post /ops/remove-modifier | Auth: Execute
Remove an attached modifier from this element by attachment ID
Removes a modifier/resource attachment by its row ID. The ID comes from the attachments or context API. This is the reverse of attach — called on the target element, not the source.
respond
Post /ops/runs/{run_id}/respond | Auth: Read
Submit a response to a pending HiTL request
Called by a respondent (member or external via token). Submits structured data matching the element’s output_schema. Each respondent can only respond once per run. When quorum is reached, the run completes with aggregated responses.
respond_view
Get /ops/respond_view | Auth: None
Serve the HITL response form for a token-authenticated respondent
Public route (no JWT required). Validates the per-respondent token, loads the HITL run context (prompt, input data, response schema), resolves the bonded View element, and renders it with injected SSR data. The rendered page includes a form that POSTs back to the respond operation with the token.
restore
Post /ops/restore | Auth: Admin
Restore element to a specific version
Automatically snapshots the current state before restoring (creates a ‘Before restore to vN’ version entry). Writes restored spec to git as .triform/spec.yaml. Git failures warn but don’t fail the operation — DB state is authoritative. Cannot restore deleted elements.
run_cancel
Post /ops/runs/{run_id}/cancel | Auth: Execute
Cancel a running execution
Only works on runs with status pending or running. Already-completed or failed runs cannot be cancelled. The run transitions to cancelled state and triggers run.cancelled event.
run_get
Get /ops/runs/{run_id} | Auth: Read
Get details of a specific run
runs
Get /ops/runs | Auth: Read
List execution runs
Filter with ?status=completed|failed|running, paginate with ?limit=N&offset=N. Default limit is 50. Returns total count for pagination.
schema
Get /ops/schema | Auth: Read
Get input/output port schemas (MCP tools/list compatible)
Call this before invoking an actor to discover its expected input/output format. Returns MCP-compatible tool definitions — useful for building dynamic tool UIs or A2A integration.
source
Get /ops/source | Auth: Read
Get any file’s content from the element’s git repository
Reads an arbitrary file from the element’s CAS-backed git tree by its relative path. Same store as
readme, just generalized. Path safety: rejects..traversal, leading/, and null bytes. Use this to viewmain.pyfor action elements, asset files for SPAs, etc. Returns empty content (not an error) if the file doesn’t exist.
source_branches
Get /ops/source/branches | Auth: Read
List Source branches for this element
Returns the standard draft/demo/live Source branches, their current commits, and promotion relationships. Use GET /api/{element_path}/ops/source/branches.
source_promote
Post /ops/source/promote | Auth: Write
Promote Source branch forward
Promotes draft to demo or demo to live through the generated element op path. Direct Git pushes to demo/live are blocked by Source policy.
source_repair
Post /ops/source/repair | Auth: Write
Inspect or repair the element Source index
Runs Source repair through the element operation path. Defaults to dry_run=true; set dry_run=false only after reviewing a dry-run report.
source_status
Get /ops/source/status | Auth: Read
Get Source control status for this element
Returns the branch-aware clone URL, checkout commands, current draft commit, child source-link count, portable export summary, Source health, warnings, and auth hints for the addressed element. Use the element-first path: GET /api/{element_path}/ops/source/status.
source_validate
Post /ops/source/validate | Auth: Read
Validate Source branch contents
Validates a Source branch before accepting local Git workflow changes or promotion. Defaults to branch=draft and rejects runtime data, generated output, secret material, and unreadable CAS refs.
stats
Get /ops/stats | Auth: Read
Get aggregate statistics for this element
Health status is computed: error if errors_per_day > 5 or success_rate < 0.8, warning if errors_per_day > 0 or success_rate < 0.95. Firing alerts escalate health to error/warning. Default period is ‘day’. Returns runs_per_day, success_rate, avg_duration_ms, and more.
submit
Post /ops/invoke | Auth: Execute
Submit a task for human response (canonical alias: invoke)
Canonical human-facing operation for HiTL elements — submits a task for human response. Equivalent to
invoke; both names POST to the same path and produce the same result. Returns a run_id for tracking. Use run_get to poll for completion, or pending to list outstanding requests. Audience MUST be defined either at the element level (spec.audience) or passed in the invoke input (input.audience). Submitting without audience in either place returns 400 InvalidInput (HITL_NO_AUDIENCE).
tree
Get /ops/tree | Auth: Read
Get the element’s position in the graph — ancestors, children, references, and subtree statistics
Uses per-circle ElementGraph cache for O(1) lookups. Returns ancestors (containment chain), children (direct), members (references), referenced_by (reverse refs), attachments, and subtree stats. Default depth is 3, max is 10. Pass ?include_metadata=true for name/state on each node.
update
Patch /ops/update | Auth: Write
Update element
Partial update — send only the fields you want to change.
spec,name, andintentionare all independently optional.specMUST be a JSON object when present; deep-merged into the existing spec by default. Empty{"spec":{}}preserves existing spec content but still records a new version (no-op for content, not for version state). To clear/replace the entire spec wholesale send{"spec":{...},"deep":false}. List-typed spec fields use replace semantics (the patch list replaces the existing list, no array merging). Coordinates Git + DB writes. Slug cannot be changed after creation.
update_meta
Patch /ops/update_meta | Auth: Write
Update element metadata (lightweight merge — does NOT bump version or snapshot spec)
Shallow JSONB merge into element.meta. Top-level keys in the provided value replace existing meta values; other keys are preserved. Used for UI metadata like canvas positions, panel state, viewer preferences. Wire-shape op_name is
update_meta(distinct fromupdate) so SSE subscribers + the cache auto-invalidator can distinguish lightweight metadata changes from spec edits without inspecting the payload. The MutatingElementStore wrapper stamps this op_name on the lifecycle event emitted byupdate_element_metastorage calls.
version
Get /ops/version | Auth: Read
Get current version or full history
Returns current version by default. Pass ?history=true for full version history (up to ?limit=N, default 50). Versions are backed by the element_versions table. Every spec update creates a new version entry.
Error Codes
| Code | Class | Retryable | Description |
|---|---|---|---|
HITL_TIMEOUT | timeout | yes | Quorum not reached before timeout |
HITL_NO_VIEW | validation | no | No View element bonded — required for rendering |
HITL_NO_AUDIENCE | validation | no | No audience defined (neither pre-configured nor in invoke input) |
HITL_INVALID_RESPONSE | validation | no | Respondent submitted data that does not match output_schema |
Lifecycle / runtime
Defined for this element
Before invoke
- validate_input
- validate_audience
- validate_view_bond
After invoke
- record_metrics
- emit_traces
On error
- log_error
- record_error_metric
Execution model: async
Observability
Defined for this element
Metrics
- request_count
- response_wait_time_ms
- pending_requests
- quorum_rate
- error_rate
Events
- hitl.invoked
- hitl.responded
- hitl.completed
- hitl.timeout
Pricing / cost
Inherited from actions
Operation costs
- invoke: 10000 micro-AU