Put a human in the loop
Pause a project, show a form to somebody, wait for them to answer, resume with their input.
Real AI systems need humans in them. Not because the AI is bad, but because judgment, taste, and domain expertise belong to people. A good AI system is a collaboration between a model that is fast and a person who steps in at the right moments.
Weft makes this a first-class feature. One node, HumanQuery, shows a form to a person, pauses the project, and waits for a response. When the person answers, the project picks up exactly where it left off, with the form values wired into the downstream ports. No webhooks, no polling, no state machine, no queue.
A tiny example
A lead-review project that shows a human the summary of each lead and asks them to approve or reject before moving on.
When execution reaches review, the project is suspended with the status waiting_for_input. A human picks it up, reads the summary, clicks Approve or Reject, maybe adds a note. The project resumes with their answer wired into the downstream ports.
How the pause works
Hitting a HumanQuery does not block a thread or hold a connection open. Weft persists the state of the whole execution (the durable execution layer handles this) and stops. No process is running, no resources are held, the project is dormant.
When the human submits the form, Weft loads the state back, plugs the form values into the node's output ports, and continues. This can take five seconds or five days. Same code path, same node, same syntax. Time is not a constraint you have to think about.
The same mechanism handles server restarts. If the host dies while a project is paused waiting for a human, the paused state is on disk. Start the server back up, the form is still pending, the human can still answer it, the execution still resumes. You do not lose runs.
Form fields
The fields config is a JSON array. Each entry has a fieldType and a key. The field type decides what UI element renders, what input ports the node exposes for upstream data, and what output ports it exposes to downstream nodes.
display: read-only area. Adds an input port{key}that you wire upstream data into. The human sees it, does not edit.display_image: read-only image. Same idea as display, but expects anImageon the input port.approve_reject: two buttons. Produces two Boolean output ports:{key}_approvedand{key}_rejected. Exactly one is non-null at runtime, depending on which button the human clicked. Optional config:approveLabel,rejectLabel.text_input: single-line text box. Produces aStringoutput.textarea: multi-line text box. Produces aStringoutput.editable_text_input/editable_textarea: pre-filled from an upstream String input. The human can keep it or edit it. Has both an input port and an output port with the same key.select: dropdown with a static list of options. Required config:options. Produces aStringoutput.multi_select: same with checkboxes, producesList[String].select_input/multi_select_input: dropdown where the options come from an upstreamList[String]input instead of a static list.
The canonical list lives in catalog/feedback/:human/query/frontend.ts (the HUMAN_FORM_FIELD_SPECS array). Tangle knows all of them and will pick the right one when you describe what you want in plain language.
Skipping the form when there is no data
Any field type that has an input port (display, display_image, editable_text_input, editable_textarea, select_input, multi_select_input) can be marked "required": true. If the data flowing into any required field is null at runtime, the entire node is skipped (same as a required port on any other node). The form is never shown, everything downstream skips via null propagation.
This is how you prevent a human from seeing an empty review form with nothing to review. The data is not ready, so the form is not raised. Default to marking any display field with upstream data as required, unless you have a reason not to.
Approve/reject as branching
The approve_reject field deserves its own moment. For a field with "key": "decision" it creates two Boolean output ports, decision_approved and decision_rejected. At runtime, exactly one is non-null (whichever button the human clicked); the other is null.
Wire each to its own downstream branch. The branch that receives null skips via null propagation. Only the chosen branch runs. No if statements, no routing logic, no conditional wiring.
This is the Weft way of doing "if approved, send the email; if rejected, log the reason", and it is the most common branching pattern in practice. Full write-up in branching.
Where does the form actually show up
A HumanQuery has two answer surfaces today. In production, the browser extension is the primary one. For testing and quick ad-hoc answers, the dashboard has a per-execution task page.
- Browser extension. This is the main surface. Team members install the extension, paste an extension token URL from the dashboard, and pending forms show up in the extension popup with a badge count on the toolbar icon. Click, review, submit, the execution resumes. Zero context switch. The node's own description says so: "shows a form to the user through their extension".
- Dashboard task page. The dashboard has a per-task page at
/tasks/<executionId>that renders the same form in the browser. This is where you land when you click a task link, and it is useful for testing without the extension. There is no inline HumanQuery popup in the builder or in the deployed public page today.
If your project is deployed at /p/<user>/<slug> and it hits a HumanQuery in the middle of a visitor's run, that execution will sit in waiting_for_input until somebody answers it from the extension or the dashboard task page. The public page itself does not render the form inline. Keep this in mind when you are designing a visitor-facing flow: if visitors should answer mid-run forms themselves, HumanQuery is not the right tool today, structure the form with Loom phase fields and a manual-run CTA instead.
The browser extension
The extension is a small icon that lights up when there is a pending HumanQuery for the logged-in user. Click it, see the form, submit the decision, it is gone.
Why a browser extension and not email: email-based approval flows always break the same way. The email gets buried, filtered, marked as spam, lost in a thread. The link expires. The approver forgets to click submit. The project waits three days for nothing. A browser extension stays out of the email pipeline, polls the server directly, and shows a count on the toolbar. Nothing to file, forward, or unread.
It also means your non-technical reviewers never need to log into the dashboard. For most production workflows, the extension is the only piece of Weft they ever touch.
Installing the extension
Two ways to get the extension onto your browser.
- Pre-built package from the dashboard. Open the
/extensionpage in your Weft dashboard. It serves pre-built packages for Chrome (.zip), Edge, Firefox (.xpi), Brave, Opera, and Safari. Download the one that matches your browser, load it as an unpacked extension (Chrome:chrome://extensions, toggle "Developer mode", drag the zip in). The packaged releases are built by./build-extension.shin the weft repo. - From source. If you are modifying the extension, run
./dev.sh extensionfrom the weft directory to build it. Load the unpacked output folder in your browser's extensions page.
Once installed, open the /extension page in your Weft dashboard, create an extension token, copy the generated URL (format <api-url>/api/ext/<token>), and paste it into the extension's connection settings. The extension polls that URL for pending tasks from then on.
Extension store distribution (Chrome Web Store, Firefox add-ons) is on the roadmap. For now the dashboard's packaged downloads are the recommended install path for team members.
HumanTrigger
The cousin of HumanQuery is HumanTrigger. Instead of pausing a running execution, it starts a fresh one from human input. Same form field specs as HumanQuery, but the form appears in the extension under the Triggers tab. Each submission kicks off a new project execution with the form data wired into the downstream nodes.
Use it when a human should kick off a workflow without logging into the dashboard. "Submit a new customer to the onboarding pipeline", "start a drafting run with this brief", "manually retry this failed lead". Once the trigger is activated it stays in the extension's Triggers tab until you deactivate it.
What's next
- Branching: the approve/reject pattern in full.
- Null propagation: the mechanism that makes branching work.
- Deploy a public page: where visitor-facing HumanQuery forms show up.