Skip to content
Academy Menu

The forms lifecycle from the CLI: create, configure, submit, transition

Author a form end to end — workflow states, permission audiences, approval routing, question logic — then drive its full response lifecycle from the CLI or API, including callback_form buttons.

deep-diveDeveloperOps12 min read

Forms in Dailybot are not just a response collector — they carry workflow states, three independent permission audiences, approval routing, and a ChatOps shortcut, and every one of those surfaces is scriptable end to end. This walkthrough builds a form from nothing, wires its lifecycle, and drives responses through it — plus the one detail that connects forms back to messaging: callback_form buttons.

Creating a form: one-shot vs. incremental

A form needs a name and at least one questioncreate with an empty question set fails with 400 questions_required. Two authoring styles, same flag vocabulary:

# One-shot: everything in a single call
FID=$(dailybot form create -n "Code Release Form" --active \
  --state "Draft:#9CA3AF" --state "Review:#F59E0B" --state "Released:#10B981" \
  --report-channel "$RELEASES_CHANNEL_UUID" \
  --approval --approver-team "Release Managers" \
  --command release \
  --questions-file q.json --json | jq -r '.uuid')

# Incremental: create bare, then shape it
dailybot form create -n "Code Release Form" --questions-file q.json --json
dailybot form config "$FID" --state "Draft:#9CA3AF" --state "Review:#F59E0B"

form config is a full partial-update: send only the flags you want to change, everything else stays as-is. It is a strict superset of form edit (name + report channels only) — prefer config for anything beyond that.

Workflow states — write shape vs. read shape

dailybot form config "$FID" \
  --state "Draft:#9CA3AF" --state "Review:#F59E0B" --state "Released:#10B981"

# Turn the workflow off entirely
dailybot form config "$FID" --no-workflow

You write {label, color} per state, ordered by flag position — the server derives key (slugified label) and order (position) for you. form get returns each state as {key, label, color, order} inside workflow.states, plus workflow.enabled: true. Passing any --state flag enables the workflow; enabling one with zero states is invalid (workflow_requires_states); max 20 states.

Permission audiences — three independent controls

--can-edit, --can-see, and --can-change-states are independent, each one of everyone / owner_and_admins / restricted:

dailybot form config "$FID" \
  --can-see everyone \
  --can-edit owner_and_admins \
  --change-states-user [email protected] \
  --change-states-team "Release Managers"

Passing a --change-states-user/-team flag implies restricted for that audience automatically. Each audience is full-replace — the users/teams you send become the entire new set; a restricted audience with empty lists means “owner + admins only.”

Approval routing and the ChatOps command

dailybot form config "$FID" \
  --approval --approver-team "Release Managers" --approver-user [email protected] \
  --command release

New submissions route through the approver list (full-replace; --no-approvers clears it, --no-approval disables routing) before counting as final. --command release binds @dailybot release as a ChatOps shortcut that opens this form — the command charset is [a-z0-9][a-z0-9_-]{0,30}, unique per org (command_already_exists if taken); --no-command unbinds it.

Public forms

dailybot form create -n "Q3 NPS Survey" \
  --anonymous --public --brand --require-identity \
  --report-channel "$SURVEYS_CHANNEL_UUID" --questions-file nps.json

--public surfaces a public_url (https://app.dailybot.com/forms/<uuid>/responses/create/) that anyone can open without a Dailybot account; null when off. Pair with --require-identity to make submitter email + name mandatory, and --brand to show the org logo. Unlike check-in anonymity, --anonymous on a form is freely toggleable in both directions.

Question authoring

The question model is exactly four types — text, multiple_choice (needs --options "A,B,C"), boolean (no options), numeric:

dailybot form questions add "$FID" --type text \
  --question "Service name?" --short-question "Service" --required

dailybot form questions add "$FID" --type multiple_choice --options "Standard,Hotfix" \
  --question "Release type?" --short-question "Type" --required

--short-question (the report title, ≤ 512 chars) is mandatory on add — pass it explicitly, or --ai-short-question to let the server generate one. On edit, the report title is not required (edits are partial). --variation adds up to 10 alternate phrasings shown to different respondents.

Conditional logic

dailybot form questions add "$FID" \
  --type multiple_choice --options "Yes,No" \
  --question "Did all tests pass?" --short-question "Tests passed" \
  --jump-if-equals "No" --jump-to 5 --else-jump-to 3

The full object (what --logic-file accepts, and what the inline flags build):

{
  "rules": {
    "rules_if": [
      {"conditions": [{"operator": "is_equal_to", "comparison_value": "No", "logic_connector": "and"}],
       "then": {"action": "jump_to", "target": 5}}
    ],
    "rules_else": {"action": "jump_to", "target": -1}
  }
}

rules_else is required; target: -1 means “jump to the end.” Jumps are forward-only — the target must exceed the current question index, or be -1. Actions are jump_to (question index), trigger_checkin (a check-in UUID), or trigger_form (chain into another form entirely — useful for routing a “Hotfix” answer into a dedicated hotfix intake form). Deleting or reordering questions auto-clamps any dangling jump targets.

# Reorder — pass the COMPLETE set of question UUIDs, in the new order
dailybot form questions reorder "$FID" "$Q3" "$Q1" "$Q2"

Reading the lifecycle vocabulary

Every response on a workflow-enabled form carries five fields — never infer a transition from a label, always read allowed_transitions:

Field Meaning
current_state The response’s effective state right now
allowed_transitions [{to_state, label}] — moves this caller can make, server-computed
can_change_state Whether the caller is in the state-change audience
allow_reopen_from_final_state Whether a terminal state is reversible
state_history Append-only {from_state, to_state, actor_name, at} log

Submitting, updating, transitioning

# Submit
dailybot form submit "$FID" --content '{"<q_summary>":"Auth service v2.1","<q_type>":"Standard"}' --yes

# Automation submission, no submitter shown, with provenance
dailybot form submit "$FID" --content '{"<q_summary>":"done"}' --yes --automation \
  --guest-name "Release Bot" --guest-email "[email protected]" \
  --source "workflow:production-deploy"

# Update an in-progress response (own responses only — never another user's)
dailybot form update "$FID" "$RID" --content '{"<q_summary>":"Auth service v2.1.1"}' --yes

# Transition once allowed_transitions is non-empty
dailybot form transition "$FID" "$RID" released --yes

--automation hides the submitter entirely in channel notifications; --anonymous replaces them with a random generated name; combined, automation wins for the notification. Neither affects who owns the response. transition accepts either the machine to_state key or the display label (case-insensitive, resolved automatically) from that response’s own allowed_transitions — never a state you merely expect to exist. Updates are strictly own-only server-side, even for admins (form_response_not_found otherwise, not a permission leak).

Connecting messaging to forms: callback_form

A send-message button with callback_form opens this exact form for the clicking user, through the normal forms UI — no external request, no payload forwarding, no bypass of the lifecycle above:

curl -sS -X POST 'https://api.dailybot.com/v1/send-message/' \
  -H 'X-API-KEY: $DAILYBOT_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "message": "Ready to log a release?",
    "target_channels": ["C0123456789"],
    "buttons": [
      {"label": "Open release form", "button_type": "interactive", "value": "open_release_form",
       "callback_form": "'"$FID"'"}
    ]
  }'

callback_form takes the form’s UUID only (list via dailybot form list --json) — names and slugs are rejected. An unknown or archived form returns 400 button_callback_form_not_found. Once opened, everything above — question types, conditional logic, workflow states, transitions — applies exactly as if the user had opened the form directly.

Cross-references

Browse the Solutions hub for the product-facing tour of the forms lifecycle.

FAQ

What's the minimum required to create a form?
A name and at least one question. dailybot form create -n NAME with no questions fails fast with 400 questions_required — seed at least one question inline via --questions-file or --interactive, or add one immediately afterward via form questions add.
How do form workflow states work?
Pass one or more --state "Label:#color" flags on create or config — passing any --state flag enables the workflow, and flag order defines the state sequence. The server derives a slugified key and a numeric order for you; you only ever write {label, color}. --no-workflow clears the states and turns the workflow off.
How is a callback_form button different from submitting a form via the CLI?
callback_form is a field on a send-message button that opens an internal Dailybot form for the clicking user to fill through the normal forms UI — no external request, no payload forwarding. It connects messaging to an existing form's own lifecycle rather than pre-filling or bypassing it.
How do I move a form response between workflow states?
dailybot form transition <form_uuid> <response_uuid> <to_state> --yes, where to_state can be either the machine key or the display label from that response's own allowed_transitions list — never a state name you assume should be available.
What's the difference between form config and form edit?
form config is a full partial-update superset covering name, workflow states, all three permission audiences, approval routing, and the ChatOps command — send only the flags you want to change. form edit only touches the name and report channels; prefer form config for anything beyond that.