Skip to main content

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).