This guide covers the main concepts for building workflows. If you’ve finished the quick start, this page explains how to design your own flows.
Workflows have two kinds of data:
Keep them separate. Business data lives in your Django models. Flow data lives in the Process model.
The Process model has a data JSONField. Use viewflow.jsonstore to
expose fields from that JSON:
from viewflow import jsonstore
from viewflow.workflow.models import Process
class HelloWorldProcess(Process):
# Stored as process.data['approved'], accessible as process.approved
approved = jsonstore.BooleanField(default=False)
class Meta:
proxy = True
Store business data in separate models. The Process model has an artifact
generic foreign key for linking to your business objects:
class MyModel(models.Model):
message = models.CharField(max_length=150)
def task_view(request, **kwargs):
form = MyModelForm(request.POST or None)
if form.is_valid():
object = form.save(commit=True)
request.activation.process.artifact = object
# additional code here..
Each task also has a data JSONField for task-specific information. This
keeps data scoped to individual tasks rather than the whole process.
Use flow.Start with a view. Viewflow provides CreateProcessView and
CreateArtifactView:
from viewflow import this
from viewflow.workflow import flow
from viewflow.workflow.flow import views
class MyFlow(flow.Flow):
start_with_artifact = (
flow.Start(views.CreateArtifactView.as_view(model=MyModel, fields=['message']))
.Annotation(title=_("Fill a form to start flow"))
.Permission("myapp.can_start_request")
.Next(this.next_task)
)
Use flow.StartHandle to start flows from code:
from viewflow import this
from viewflow.workflow import flow
class MyFlow(flow.Flow):
start_noninteractive = flow.StartHandle(this.start_process).Next(this.approve)
def start_process(self, activation, message=''):
object = MyModel.objects.create(message=message)
activation.process.artifact = object
return activation.process
# Start from anywhere in your code
process = MyFlow.start_noninteractive.run(message="Hello World")
User tasks need human input. Use flow.View with built-in views like
UpdateProcessView or UpdateArtifactView:
class MyFlow(flow.Flow):
...
approve = (
flow.View(views.UpdateProcessView.as_view(fields=["approved"]))
.Permission(auto_create=True)
.Next(this.check_approve)
)
Use flow.ManualTask() for work performed outside any system – the task
appears in the task list and the user marks it done with a plain
confirmation form:
greet_client = flow.ManualTask().Next(this.end)
Runs a function immediately when the previous task finishes. Runs in the same database transaction:
from viewflow import this
from viewflow.workflow import flow
class MyFlow(flow.Flow):
process_class = MyProcess
start = (
flow.Start(...)
.Next(this.process_data)
)
process_data = (
flow.Function(this.process_data_function)
.Next(this.end)
)
def process_data_function(self, activation):
activation.process.sample_text = activation.process.sample_text.upper()
activation.done()
end = flow.End()
flow.SendHandle and flow.BusinessRule work the same way but export
as dedicated BPMN task types – an outbound message hook (send task) and a
rule evaluation (business rule task):
send = flow.SendHandle(this.notify_customer).Next(this.discount)
discount = flow.BusinessRule(this.calc_discount).Next(this.end)
Waits for external code to call it. Use this for webhooks or external events:
from viewflow import this
from viewflow.workflow import flow
class MyFlow(flow.Flow):
process_class = MyProcess
start = (
flow.Start(...)
.Next(this.wait_for_external_event)
)
wait_for_external_event = (
flow.Handle(this.handle_external_event)
.Next(this.end)
)
def handle_external_event(self, activation, data):
activation.process.sample_text = data['new_text']
activation.done()
end = flow.End()
# Call from external code
process = MyFlow.wait_for_external_event.run(
process=my_process_instance,
data={'new_text': 'Updated Text'}
)
Runs a task in Celery for long-running work:
# tasks.py
from celery import shared_task
from viewflow.flow import flow_job
@shared_task
def send_hello_world_request(activation_ref):
with Job.activate(activation_ref) as activation:
result = 'Background Processing Done'
# No locks during long jobs - save carefully
activation.process.sample_text = result
activation.process.save(updated_fields=['sample_text'])
See also
Waits until a moment stored in the database, so pending timers survive a message broker restart or flush:
class MyFlow(flow.Flow):
...
wait = flow.Timer(timedelta(days=1)).Next(this.escalate)
...
The delay accepts a timedelta, an absolute datetime, or a callable
activation -> timedelta | datetime. Due timers are fired by a periodic
dispatcher – run the workflow_timers management command from cron, or
schedule the viewflow.workflow.tasks.workflow_fire_timers task with
celery beat:
app.conf.beat_schedule = {
"viewflow-timers": {
"task": "viewflow.workflow.tasks.workflow_fire_timers",
"schedule": 60.0,
},
}
Starts a new process on a schedule, fired by the same dispatcher:
class ReportFlow(flow.Flow):
start = flow.StartTimer(interval=timedelta(days=1)).Next(this.report)
...
For cron-style schedules, point celery beat or OS cron directly at
MyFlow.start.run().
Intermediate events sit between tasks. Catch events wait; throw events fire and continue.
flow.MessageCatch waits for external code, like flow.Handle but
exported as a message event; flow.MessageThrow runs an outbound hook:
wait_payment = flow.MessageCatch(this.on_payment).Next(this.notify)
notify = flow.MessageThrow(this.send_receipt).Next(this.end)
flow.SignalThrow broadcasts a named signal to every armed
flow.SignalCatch, across all processes and flows – one throw releases
them all:
# in one flow
shipped = flow.SignalThrow("order-shipped").Next(this.end)
# in another
wait = flow.SignalCatch("order-shipped").Next(this.invoice)
flow.ConditionalCatch waits until a condition over the process data
becomes true. It is evaluated by the workflow_timers dispatcher, the
same sweep that fires flow.Timer:
wait = flow.ConditionalCatch(
lambda activation: activation.process.approved
).Next(this.proceed)
flow.EscalationThrow raised inside a subprocess notifies the parent’s
non-interrupting .OnEscalation boundary without stopping either process
– see Boundary Events below.
A boundary event attaches to a task and fires while it is still active.
Chain .OnTimeout / .OnError / .OnEscalation onto the host task,
before .Next(). Each creates an auto-named boundary node
(<task>__timeout, <task>__error, <task>__escalation):
class MyFlow(flow.Flow):
approve = (
flow.View(...)
.OnTimeout(timedelta(days=3), this.escalate)
.Next(this.end)
)
...
.OnTimeout(delay, then) fires when the delay elapses first – deadlines
and escalations. .OnError(then, code=...) fires when the host task fails
in a background context (job, function, timer) and routes to a recovery path:
deploy = flow.Function(this.run_deploy).OnError(this.rollback).Next(this.end)
By default a boundary event interrupts (cancels) its host task; pass
interrupting=False to start a parallel path and leave the host running.
Pass title= to label the boundary in the chart. Boundary events are
canceled automatically when the host task completes. Timer boundaries are
fired by the same workflow_timers dispatcher as flow.Timer.
.OnEscalation(then, code=...) catches a flow.EscalationThrow raised
inside a subprocess. It never interrupts – the subprocess and the child
both keep running:
sub = (
flow.Subprocess(OrderFlow.start)
.OnEscalation(this.notify_manager, code="over-budget")
.Next(this.end)
)
BPMN separates tasks (do something) from gates (decide what’s next). This makes workflows easier to understand and modify.
Takes one of two paths based on a condition:
from viewflow import this
from viewflow.workflow import flow
class MyFlow(flow.Flow):
...
check_condition = (
flow.If(lambda activation: activation.process.approved)
.Then(this.approved_task)
.Else(this.rejected_task)
)
...
See also
Switch for more than two branches
Split creates parallel branches. Join waits for all branches to finish:
from viewflow import this
from viewflow.workflow import flow
class MyFlow(flow.Flow):
process_class = MyProcess
start = (
flow.Start(...)
.Next(this.parallel_tasks)
)
parallel_tasks = (
flow.Split()
.Next(this.task1).Next(this.task2)
)
task1 = flow.View(...)
.Next(this.join)
task2 = flow.View(...)
.Next(this.join)
join = flow.Join()
.Next(this.next_step)
next_step = flow.End()
See also
PRO-only
Use flow.Subprocess and flow.NSubprocess to break large flows into
smaller, reusable pieces. flow.NSubprocess(..., sequential=True) runs
one child process at a time instead of all at once.
See also
See also
Use flow.End to finish a process. You can have multiple end nodes for
different outcomes:
class MyFlow(flow.Flow):
# previous flow steps...
check_approve = (
flow.If(act.process.approved)
.Then(this.approved)
.Else(this.rejected)
)
approved = flow.End()
rejected = flow.End()
flow.TerminateEnd() cancels all other active tasks and finishes the
process immediately, without waiting for parallel branches.
flow.ErrorEnd(code) does the same and records the process as failed.
Inside a subprocess, the parent task is marked ERROR so the parent can
catch it with .OnError(this.recover, code=...) on the subprocess.
Register an undo handler on a task and run all handlers in reverse
completion order with flow.CompensateThrow:
book_hotel = (
flow.Function(this.do_book)
.CompensateWith(this.cancel_hotel)
.Next(this.pay)
)
cancel_hotel = flow.Function(this.do_cancel)
...
compensate = flow.CompensateThrow().Next(this.fail_end)
fail_end = flow.ErrorEnd("payment-failed")
Each completed task is compensated at most once. A handler is a
flow.Function with no incoming connections; it receives the compensated
task as activation.task.previous.
Every flow is exportable as a standard BPMN 2.0 file that opens in bpmn.io or Camunda Modeler. See BPMN Export.