Download all docs
actions

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.

Hi
type

Human in the Loop

Collect structured human input via a connected View

actionsatomdefinition

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

PropertyValue
Typehitl
Categoryapps
Formatom
Symbolperson_check / #EC4899

Properties

FieldTypeDefaultDescription
descriptionstringDescription shown to the approver
promptstringQuestion or instruction for the approver (required)
optionsarraySee belowResponse 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.usersarraySpecific user references who can respond
assignees.rolesarrayRole names — any member of the role can respond
assignees.policystring (enum)anyApproval policy: any (first response wins) or all (all must respond)
timeout.duration_hoursinteger24Hours before timeout action triggers
timeout.actionstring (enum)failAction on timeout: fail, approve, reject, or escalate
notification.channelsarray["email"]Notification channels: email, slack, sms
notification.reminder_hoursintegerHours before the deadline to send a reminder
context.include_inputsbooleantrueInclude the flow’s input data in the approver’s view
context.include_previous_outputsbooleantrueInclude previous step outputs in the approver’s view
context.custom_fieldsarrayCustom fields to display. Each item: label, expression (CEL)

Ports

DirectionPortTypeRequiredDescription
InputrequestrequestYesApproval request with prompt and optional form schema
OutputresponserequestYesApproval 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

CapabilityDescription
approval-workflowHuman approval workflows
form-collectionStructured data collection from approvers
timeout-handlingConfigurable timeout behavior
notificationsMulti-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

ModifierUse case
auth-policyRestrict which users or roles can see and respond to approval requests
requirementsRequire 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

PropertyTypeDefaultDescription
promptstringDefault 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.
audienceobjectPre-defined respondents. Can be overridden or supplemented at invoke time.
quorumobjectHow many responses are required to complete the action
timeoutobjectWhat happens if quorum is not reached in time
notificationobjectHow 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 view main.py for 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, and intention are all independently optional. spec MUST 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 from update) 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 by update_element_meta storage 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

CodeClassRetryableDescription
HITL_TIMEOUTtimeoutyesQuorum not reached before timeout
HITL_NO_VIEWvalidationnoNo View element bonded — required for rendering
HITL_NO_AUDIENCEvalidationnoNo audience defined (neither pre-configured nor in invoke input)
HITL_INVALID_RESPONSEvalidationnoRespondent 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