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 belowstate— optional initial values that pre-fill widgetshide_steps— hides the step counter shown at the top- Returns a dict with all widget values, keyed by each widget's
key=
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])
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])
Button(label, key=None)—keydefaults to the label if omittedstate[key]is alwaysTrue(boolean) when a button is clicked — not a string- Returning
Nonefrom a button-handling branch advances the form silently NextButton()andBackButton()are added automatically; only instantiate them explicitly when mixing them with custom buttons:[Button("Save draft"), NextButton()]ExitButton()terminates the process viasys.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"]returnsNone— noKeyError(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 fieldstask.complete()— mark as done manually (not needed insidewith task:)
Never use get_trigger_task() in a Form — that function is Tasklet-only. Forms always use get_tasks().
Common Mistakes
| Mistake | Why it's wrong | Correct approach |
|---|---|---|
Multiple run() calls for a multi-step flow | State is lost between calls, no Back | Single run([step1, step2, ...]) |
| Side effects inside a reactive page | Runs on every keystroke | Move to a computation step (no return) |
get_trigger_task() in a Form | Forms don't have trigger tasks | get_tasks() |
No key= on widgets | Auto-generated keys break on reorder | Always set an explicit key= |
state["btn"] == "btn" | Button sets boolean True, not a string | if state.get("btn"): |
| Input widgets inside a generator step | No user interaction during yield | Output widgets only (MarkdownOutput, ProgressOutput, HtmlOutput) |