Form-Based Transitions
A form-based transition opens a form drawer before the transition executes, collecting input from the user. The data captured is associated with the WorkflowTransaction record for that transition.
When to Use
Use a form-based transition when the transition needs additional context — an approval note, a rejection reason, an effective date, a supporting file upload, etc.
Creating a Transition Form
Inherit from BaseSimpleForm for forms that don't map to a model field, or BaseForm for model-based forms.
BaseSimpleForm (most common for transitions)
from django import forms
from ...packages.crud.forms import BaseSimpleForm
class ApprovalForm(BaseSimpleForm):
notes = forms.CharField(label="Approval Notes", required=True)
effective_date = forms.DateField(label="Effective Date", required=True)
class Meta:
title = "Approve Record"
order = ["effective_date", "notes"] # field display order
def save(self):
# Access the record being transitioned
object_instance = self.initial.get("object_instance")
object_instance.approval_notes = self.cleaned_data.get("notes")
object_instance.save()
For Textarea / Rich Widgets
Use extra_ui_schema on the field in __init__:
class RejectionForm(BaseSimpleForm):
reason = forms.CharField(label="Rejection Reason", required=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.declared_fields["reason"].extra_ui_schema = {
"ui:widget": "TextareaFieldWidget",
"ui:options": {"rows": 3},
}
class Meta:
title = "Reject Record"
order = ["reason"]
def save(self):
obj = self.initial.get("object_instance")
obj.rejection_reason = self.cleaned_data.get("reason")
obj.save()
Attaching the Form to a Transition
Add the form class to the transition dictionary with the "form" key:
status_transitions = [
{
"name": "approve",
"display_name": "Approve",
"from": "pending",
"to": "active",
"roles": ["Manager", "Admin"],
"form": ApprovalForm, # ← attach here
},
{
"name": "reject",
"display_name": "Reject",
"from": "pending",
"to": "rejected",
"roles": ["Manager", "Admin"],
"form": RejectionForm,
},
]
When a user clicks the transition button, a form drawer opens. On submit, the save() method runs, then the transition executes and the record moves to the new status.
Import Paths
# BaseSimpleForm — no model required
from ...packages.crud.forms import BaseSimpleForm
# BaseForm — model-mapped fields
from ...packages.crud.forms import BaseForm
The same relative import depth rules apply as for CRUD forms (3 dots for flat modules, 4 for nested).