Skip to main content

Step Types

A Form is a Python script that calls run(steps). The runtime loops through each step: render → wait for user input → advance. There are five step types that cover every UI pattern you'll need.

from abstra.forms import run

state = run(steps, state={}, hide_steps=False)
  • steps — a list mixing any of the five step types below
  • state — optional initial values that pre-fill widgets
  • hide_steps — hides the step counter shown at the top
  • Returns a dict with all widget values, keyed by each widget's key=
info

Put all steps in a single run() call. State is shared across steps within one call, and the Back button only works within a single call. Multiple run() calls are isolated sessions — state is lost between them.


1. Static page

A plain list of widgets. Renders the same content every time — use when nothing needs to change based on previous input.

from abstra.forms import run, TextInput, NumberInput

run([
[
TextInput(key="name", label="Full name"),
NumberInput(key="age", label="Age"),
]
])

2. Reactive page

A function that returns a list of widgets. Re-executes on every user interaction (every keystroke, every click). Use for dynamic content, conditional fields, and live validation.

from abstra.forms import run, DropdownInput, TextInput

def page(state):
widgets = [
DropdownInput(key="role", label="Role", options=["Engineer", "Manager"]),
]
if state.get("role") == "Manager":
widgets.append(TextInput(key="team_size", label="Team size"))
return widgets

run([page])
warning

Never put side effects in a reactive page. DB writes, API calls, and send_task would run on every keystroke. Use a computation step for those.


3. Reactive page with custom buttons

Return a (widgets, buttons) tuple to replace the default Next/Back with your own buttons. When a button is clicked, state[key] is set to True (boolean) and the function re-executes.

from abstra.forms import run, MarkdownOutput, TextInput, Button, NextButton

def approval(state):
if state.get("approve"):
do_approve()
return # returning None advances silently (acts as a computation step)

if state.get("reject"):
do_reject()
return

return [
MarkdownOutput("Review the request and decide:"),
TextInput(key="note", label="Note", required=False),
], [Button("Approve", key="approve"), Button("Reject", key="reject")]

run([approval])
info
  • Button(label, key=None)key defaults to the label if omitted
  • state[key] is always True (boolean) when a button is clicked — not a string
  • Returning None from a button-handling branch advances the form silently
  • NextButton() and BackButton() are added automatically; only instantiate them explicitly when mixing them with custom buttons: [Button("Save draft"), NextButton()]
  • ExitButton() terminates the process via sys.exit(0) — use for Cancel flows

4. Computation step

A function with no return value. Runs exactly once when the user navigates through it. No UI is shown. This is the correct place for all side effects: database writes, sending emails, calling APIs, dispatching tasks.

from abstra.forms import run, TextInput, MarkdownOutput
from abstra.tables import insert
from abstra.tasks import send_task

def save(state):
record = insert("employees", {"name": state["name"], "age": state["age"]})
send_task("onboarding", {"employee_id": record["id"]})
state["employee_id"] = record["id"]
# no return = computation step

run([
[TextInput(key="name", label="Name"), NumberInput(key="age", label="Age")],
save,
[MarkdownOutput("Done!")],
])

5. Generator step

A function that yields lists of widgets. Use for progress bars or status updates during long operations. Each yield replaces the previous UI — no user interaction is possible between yields. Only output widgets make sense here (ProgressOutput, MarkdownOutput, HtmlOutput).

from abstra.forms import run, FileInput, ProgressOutput, MarkdownOutput
from abstra.ai import prompt
from time import sleep

upload_page = [FileInput(key="file", label="Upload document")]

def process(state):
yield [MarkdownOutput("Processing..."), ProgressOutput(current=1, total=3, text="Reading file...")]
sleep(1)
yield [MarkdownOutput("Processing..."), ProgressOutput(current=2, total=3, text="Running AI...")]

result = prompt([state["file"].path, "Summarize this document."])
state["summary"] = result

yield [MarkdownOutput("Processing..."), ProgressOutput(current=3, total=3, text="Done!")]

def result_page(state):
return [MarkdownOutput(f"## Summary\n\n{state['summary']}")]

run([upload_page, process, result_page])

Live Validation

Any widget accepts errors: list[str]. Set it inside a reactive page. Widgets with errors block the Next button.

from abstra.forms import run, TextInput

def page(state):
name = state.get("name", "")
errors = ["Must be at least 2 words"] if name and len(name.split()) < 2 else []
return [TextInput(key="name", label="Full name", errors=errors)]

run([page])

State

State is a dict shared across all steps within one run() call.

  • state["missing_key"] returns None — no KeyError (state is a dict subclass)
  • Widget values are stored under their key= parameter
  • Computation steps can write to state: state["id"] = record["id"]

Tasks in Forms

Forms receive tasks via get_tasks(). Use the with task: context manager — it auto-completes the task on success and leaves it open if an exception is raised.

from abstra.tasks import get_tasks

tasks = get_tasks()

for task in tasks:
with task:
process(task["data"]) # task auto-completed at end of with block
  • task["key"] — access payload fields
  • task.complete() — mark as done manually (not needed inside with task:)
warning

Never use get_trigger_task() in a Form — that function is Tasklet-only. Forms always use get_tasks().


Common Mistakes

MistakeWhy it's wrongCorrect approach
Multiple run() calls for a multi-step flowState is lost between calls, no BackSingle run([step1, step2, ...])
Side effects inside a reactive pageRuns on every keystrokeMove to a computation step (no return)
get_trigger_task() in a FormForms don't have trigger tasksget_tasks()
No key= on widgetsAuto-generated keys break on reorderAlways set an explicit key=
state["btn"] == "btn"Button sets boolean True, not a stringif state.get("btn"):
Input widgets inside a generator stepNo user interaction during yieldOutput widgets only (MarkdownOutput, ProgressOutput, HtmlOutput)