Question types
Every human interaction is modeled as aQuestion with a type that determines how it’s presented:
| Type | Description | CLI presentation |
|---|---|---|
YesNo | Binary yes/no decision | [Y/N] prompt |
Confirmation | Confirm an action (like YesNo) | [Y/N] prompt |
MultipleChoice | Pick one option from a list | Arrow-key selector or numbered list |
MultiSelect | Pick one or more from a list | Checkbox selector |
Freeform | Open-ended text input | > prompt |
Question structure
Each question carries metadata beyond the prompt text:| Field | Description |
|---|---|
text | The question displayed to the user |
question_type | One of the types above |
options | List of {key, label} pairs for choice questions |
allow_freeform | Whether free-text input is accepted in addition to fixed options |
default | Default answer used on timeout |
timeout_seconds | How long to wait before using the default or timing out |
stage | The node ID that generated this question |
metadata | Arbitrary key-value metadata for integrations |
Answer values
Answers are one of six variants:| Value | Meaning |
|---|---|
Yes | Affirmative response to a yes/no or confirmation question |
No | Negative response |
Selected(key) | A specific option was chosen (carries the option key) |
Text(string) | Free-text input |
Skipped | The user dismissed the question without answering |
Timeout | The question’s timeout elapsed without a response |
selected_option (the full {key, label} pair) and a text field for freeform input.
How human gates build questions
When the engine reaches a human gate node (shape=hexagon), the human handler builds a question from the node’s outgoing edges:
- Each edge becomes an option, with the accelerator key parsed from the label (e.g.
[A] Approve→ keyA, label[A] Approve) - Edges with
freeform=trueare excluded from the option list and enable free-text fallback - The question text comes from the node’s
labelattribute
Channels
TheInterviewer trait has a simple interface — ask(question) → answer — and Fabro provides implementations for each delivery channel:
Console
The default for CLI runs. On a TTY, the console interviewer uses interactive widgets (arrow-key selection, checkbox multi-select, confirm prompts) viadialoguer. When stdin is piped (non-TTY), it falls back to a line-based reader with numbered options.
Web
The default for API server runs. The web interviewer holds questions in a queue until answers are submitted externally — typically by the web UI or a REST API call. Each question gets a unique ID (e.g.q-1), and the ask() call blocks on a oneshot channel until submit_answer(id, answer) is called.
This decoupling means the workflow engine and the user interface can run in different processes. The web UI polls for pending questions and posts answers back to the API.
Slack
Fabro’s Slack integration uses the web interviewer under the hood. When a human gate fires, the pending question is rendered as a Slack message with interactive buttons. When a user clicks a button, the Slack event handler callssubmit_answer() on the web interviewer, unblocking the workflow.
Auto-approve
For fully automated runs or CI pipelines, the auto-approve interviewer answers every question without human input:YesNo/Confirmation→YesMultipleChoice/MultiSelect→ first optionFreeform→"auto-approved"
--auto-approve flag:
Timeouts
Questions can have atimeout_seconds field. When set, Fabro wraps the interviewer call with a timeout:
- If the user answers before the deadline, their answer is used normally
- If the timeout elapses and a
defaultanswer is set on the question, the default is used - If the timeout elapses with no default, the answer is
Timeout
human.default_choice attribute. If set, execution continues to the default target. Otherwise, the stage retries.