Skip to content

Forms vs FSM vs Scenes

Status: Established design guidance.

The canonical rule:

Forms collect data.
FSM remembers durable workflow state.
Scenes organize longer workflows.

Forms — collect data in ONE interaction

A Google Chat dialog/form collects several fields at once. The user fills the fields and submits; the app receives typed inputs in a single SUBMIT_DIALOG event.

@dataclass
class RegistrationForm(FormModel):
    name: StringInput
    email: StringInput
    department: StringInput


@router.dialog_submit(RegistrationForm.filter())
async def submit(event: ActionEvent, form: RegistrationForm) -> ActionStatus:
    return ActionStatus.ok(f"Saved: {form.name.values[0]}")

Use Forms when: the data is collected in one sitting, the fields are known upfront, and nothing needs to survive beyond the submit.

FSM — remember durable workflow state

FSM stores state that must survive across messages, users, time, external callbacks, restarts, or long-running approval flows. Use it when the workflow CANNOT be one interaction: approval chains, external API callbacks, multi-session processes.

class RequestFlow(StatesGroup):
    pending_approval = State()
    done = State()


# after the form submit:
await storage.compare_and_set(
    key,
    expected_revision=0,
    replacement=FSMRecord(state=RequestFlow.pending_approval.state, data=...),
)

FSM is not the default for structured data collection. A Telegram-style message-by-message registration is an incremental alternative, not the Google-native first choice (see the FSM workflow in examples/finite_state_machine.py vs the typed Dialog alternative in examples/docs/from_zero.py).

Scenes — organize longer workflows

Scenes can layer over FSM to structure long multi-step workflows (enter/exit/fallback/timeout/reset). A Scene is never a second persistence engine.

Decision table

Situation Use
Collect N known fields in one sitting Form / Dialog
One-shot submit, then done Form only
State must survive restart/approval/callback Form (collect) + FSM record (remember)
Telegram-style message-by-message flow must be migrated FSM (sequential) — document the Form alternative
Long multi-step workflow organization Scenes over FSM
Polls APPLICATION: command → private Dialog → public Card → Actions/domain storage (not shipped/core)

Canonical examples: examples/docs/from_zero.py (typed form + dialog) and examples/finite_state_machine.py (sequential FSM). Polls are application-owned scenarios assembled from the primitives.

Cross-context FSM access (explicit keys)

A manager advancing an employee's request is ordinary application code: address the workflow record by an EXPLICIT StorageKey on the record store — never a privileged global "modify another user's FSM" operation.

key = StorageKey(user="users/9", space="spaces/A", thread=None)
record = await storage.get_record(key)
await storage.compare_and_set(
    key, record.revision, FSMRecord(state="approved", data=record.data)
)

Response lifecycle (sync vs deferred)

The interaction response (30 s, no outbound credentials) and authenticated Bot calls are DIFFERENT channels. Handlers must answer the interaction quickly (text/card/dialog) and move long CRM/browser/AI work into Bot calls — no ack() API, no queue: the boundary IS the API split.

Onboarding recipe (ADDED_TO_SPACE)

@router.added_to_space()
async def welcome(event: AddedToSpaceEvent) -> str:
    return "Hi! I can: /search — search, Report — export, buttons — requests."

Recipe only — onboarding is not a lifecycle engine.

Adjacent mental models

  • return "pong" / InteractionResponse.respond(...) — answer the CURRENT interaction (30 s, no outbound credentials). bot.app.messages.create(space, ...) — explicit proactive outbound (authenticated, any Space). They are different channels; Chattice never merges them.
  • One-way notification to ONE Space → an incoming webhook MAY be enough. One app identity sending to MULTIPLE Spaces and/or handling interactions → full Chattice app (see docs/guides/enterprise.md).
  • UI navigation between Cards ≠ FSM state. Dialog/Form field values ≠ durable workflow state: a form submission becomes FSM data only when the workflow must survive messages/users/time/restarts.