title: User Task description: A human step: assign, list, complete.
User Task¶
A User Task is a step a person performs. When execution reaches it the
engine parks the task — it releases the track goroutine and announces the task
to a TaskDistributor — and waits: an eligible actor claims it, fills in its
form, and completes it. Only then does the token flow on. This page is the
developer reference: the type, its constructor, every option, the Renderer
contract, and its wait-node runtime behavior.
Taxonomy¶
| BPMN category | Activity → Task → User Task (§10.3.3) |
| Package | github.com/dr-dobermann/gobpm/pkg/model/activities |
| Type | activities.UserTask |
| Inherits | the Activity attributes and associations — I/O sets, boundary events, loop characteristics, compensation |
| Implements | flow.Node, exec.NodeExecutor (Exec), exec.NodeDataConsumer/Producer (LoadData/UploadData), flow.ActivityNode (ActivityType, AddBoundaryEvent) |
| The work | a human, driven by a TaskDistributor, filling in a hinteraction.Renderer form |
Where it sits in the activity family: Activities taxonomy.
Constructor¶
func NewUserTask(
name string,
userTaskOpts ...options.Option,
) (*UserTask, error)
| Parameter | Meaning |
|---|---|
name |
the task's diagram name (and default id source). |
userTaskOpts |
zero or more options (below) — the assignment triad, renderer(s), and declared outputs. |
It returns an error — never panics — on an invalid option (e.g. a nil renderer, or an assignment slot given both static and expression forms).
Options¶
Most User Tasks need only these:
| Option | When you reach for it |
|---|---|
WithCandidateUsers(ids…) |
a pool of eligible users; the first to claim it owns it. |
WithAssignee(id) |
a single owning user — only that user may act. |
WithRenderer(r) |
attach the form the human fills in. |
WithOutput(name, type, required) |
declare a value the form collects. |
WithoutParams() |
the task declares no process I/O parameters. |
The full set comes from three families — user-task options (the assignment triad, renderers, outputs), the activity options any activity accepts, and the foundation/data options.
User-task options decide who may act and what the form collects. Each
assignment slot has a static form and an expression form (resolved from a
data.FormalExpression when the task is announced — once per iteration for an
iterated task); the two forms are mutually exclusive:
| User-task option | Effect |
|---|---|
WithAssignee(userID string) |
single owning user; only a matching UserID is authorized (the restrictive gate). |
WithAssigneeExpr(expr data.FormalExpression) |
the assignee, resolved at announcement. |
WithCandidateUsers(userIDs ...string) |
pool of eligible users; a matching user is authorized. |
WithCandidateUsersExpr(expr data.FormalExpression) |
the candidate users, resolved at announcement. |
WithCandidateGroups(groupIDs ...string) |
any member of an intersecting group is authorized. |
WithCandidateGroupsExpr(expr data.FormalExpression) |
the candidate groups, resolved at announcement. |
WithRenderer(r hinteraction.Renderer) |
attach a form; call it more than once for multiple renderings (deduplicated by identity). |
WithOutput(name, pType string, required bool) |
declare a value the form collects; required: true rejects completion without it. |
WithTaskPriority(priority int) |
BPMN's taskPriority (Table 10.14), reported to your distributor on TaskInfo. The engine acts on it nowhere — sort your own inbox by it. Any int, zero by default. |
Roles: BPMN's own vocabulary. Beside the Camunda-style triad, a UserTask
accepts the standard's resource roles through the activity option
WithRoles(...), and a declared human role decides eligibility just as a
triad slot does:
| Role constructor | Authorizes? |
|---|---|
hinteraction.NewPotentialOwner(name, nil, expr, nil) |
yes — "persons who can claim and work on" the task |
hinteraction.NewHumanPerformer(name, nil, expr, nil) |
yes — the human specialization of Performer |
hinteraction.NewPerformer(name, …) |
no — a Performer may name a machine or a department |
hinteraction.NewResourceRole(name, …) |
no — a bare role is documentation |
The role's expr is a ResourceAssignmentExpression wrapping a
data.FormalExpression that resolves to identifiers. Two things differ from a
triad slot, both following from the standard:
- An identifier matches a user id or a group. BPMN's expressions return
"Users or Groups" and mark neither, and a
ResourceRolehas nowhere to record which — so both halves of the actor's identity are checked. Need the distinction? Use the triad; that is what it is for. - A role naming nobody is refused where you write it. An authorizing role
with neither an assignment expression nor a
resourceRefcould only authorize nobody, so the constructor rejects it. A role usingresourceRef(a query into an organizational directory) is refused when the process is registered — gobpm has no directory to query.
Roles compose with the triad as a union, under the same precedence: a
declared assignee still excludes everything else, and a task is open to
anyone only when neither a triad member nor a human role is declared.
owners, _ := hinteraction.NewResourceAssignmentExpression(reviewersExpr)
role, _ := hinteraction.NewPotentialOwner("reviewers", nil, owners, nil)
ut, _ := activities.NewUserTask("approve",
activities.WithRoles(role),
activities.WithTaskPriority(5),
activities.WithOutput("decision", "string", true),
activities.WithoutParams())
Activity options (shared by every activity):
| Activity option | Effect |
|---|---|
WithParameters(dir data.Direction, params ...*data.Parameter) |
declare typed inputs/outputs. |
WithoutParams() |
declare no parameters. |
WithCompensation() |
mark the task a compensation handler (armed, off the normal flow). |
WithLoop(lc) / WithMultyInstance() |
repeat the activity — see Standard Loop, Multi-Instance. |
WithStartQuantity(n) / WithCompletionQuantity(n) |
BPMN token quantities (default 1). |
Foundation & data options: WithID, WithDoc, WithProperties.
Boundary events are attached with the method
AddBoundaryEvent, not a constructor option — see Boundary events.
For the complete, always-current signatures run
go doc github.com/dr-dobermann/gobpm/pkg/model/activities.
The Renderer contract¶
The form is a hinteraction.Renderer. On completion the TaskDistributor
calls Render to collect the outputs the human supplies:
type Renderer interface {
foundation.Identifyer
foundation.Documentator
// Implementation returns the type of implementation it provides.
Implementation() string
// Render shows a data form to the user and gathers the user's inputs,
// returned as a data.Data slice. ds provides values needed for rendering.
Render(ds data.Source) ([]data.Data, error)
}
You implement Render (show the form, return the collected data.Data) and
Implementation (the rendering type). The package ships a console renderer,
consinp, so you need not write one to get started — build one with
consinp.NewRenderer and declare its fields with WithStringInput /
WithIntInput / WithMessager, and (for a scripted run) WithSource.
Build it¶
Build the task with its assignment, form, and declared output, then wire it
like any other node. Here approve is claimable by the candidate user
operator and collects a required decision string (from
examples/usertask/):
form, _ := consinp.NewRenderer(
consinp.WithStringInput("decision", "Approve? type a decision"),
consinp.WithSource(bytes.NewBufferString("approved\n")),
)
ut, _ := activities.NewUserTask("approve",
activities.WithCandidateUsers("operator"),
activities.WithRenderer(form),
activities.WithOutput("decision", "string", true),
activities.WithoutParams(),
)
WithSource feeds the console renderer a scripted answer so the example runs
end-to-end with no interactive typing.
Run it¶
A User Task needs a bound TaskDistributor — the component that surfaces
parked tasks to humans. The example uses the built-in console driver, acting
as an operator whose UserID() matches the task's candidate user:
driver := console.New(operator{}, os.Stdout)
th, _ := thresher.New("approval-engine",
thresher.WithTaskDistributor(driver))
driver.Bind(th)
console.New(actor, w) builds the driver acting as a specific human;
WithTaskDistributor registers it so the engine can announce parked tasks; and
driver.Bind(th) links the driver back to the engine so it can call Take and
Complete — the two halves must know each other. Running
examples/usertask/:
task available: id=6595855523137331530 node=3424710742457493363
TaskState Announced node_name=approve task_id=6595855523137331530
TaskState Taken node_name=approve task_id=6595855523137331530
TaskState Claimed user_id=operator task_id=6595855523137331530
Approve? type a decision
TaskState Completed node_name=approve task_id=6595855523137331530
task withdrawn: id=6595855523137331530
task completed: id=6595855523137331530
process finished: Completed
The engine announces the task (Announced), the driver takes it
(Taken), claims it (Claimed) so no other candidate can work it in
parallel, renders the form, and completes it (Completed); the collected
decision becomes task output and the token flows to end.
Claiming is required¶
A task is completable only by its actual owner, so an unclaimed task is
completable by nobody — Claim is a step, not a courtesy:
| Call | Meaning |
|---|---|
th.Claim(ctx, taskID, actor) |
take exclusive hold. Refused if someone else holds it; re-claiming your own task is a harmless no-op, so claim-before-complete is safe to retry. |
th.Unclaim(ctx, taskID, actor) |
give it back to the pool. Only the holder may release. |
th.Reassign(ctx, taskID, userID) |
move it to someone else, overriding the current holder. The engine does not check the caller — its callers are managers and administrators, not participants — so your application decides who may do this, and should log who did. The nominee is still checked against the task's triad. |
A task assigned to exactly one person (WithAssignee, or an expression
resolving to one id) is born owned by them — no claim needed, though
Unclaim and Reassign still work on it.
When a completion is refused, the error says which rule refused it:
TaskUnclaimedClass (claim it first), TaskNotOwnerClass (someone else holds
it), or a plain authorization failure (you are not a candidate at all).
Who performed the task¶
Completion records the performer where a later node can read it — the
"send it to the approver's manager" pattern. It lives in the engine's read-only
RUNTIME area, keyed by node name:
RUNTIME/COMPLETED_BY → map: node name → the user who completed it
Read-only matters: your process can route on the record but cannot forge or overwrite it. It survives dehydration, so a task completed before a weekend's wait is still attributable afterwards. A looped task overwrites its entry each pass, so the map names the last completer.
Without a bound
TaskDistributora User Task still parks and is completable by id, but nothing surfaces it — the default distributor is a no-op. The human step never advances until something drives it.
The task distributor as an operating concern has its own page: Human tasks.
Methods & runtime behavior¶
The engine drives the task through these — you rarely call them directly:
| Method | Role |
|---|---|
Exec(ctx, re) ([]*flow.SequenceFlow, error) |
reached once, after acceptance: bind the completed outputs into the frame and advance onto the outgoing flow(s). It never blocks. |
Authorize(ctx, actor, src, eng) |
verdict on whether actor may act, per the assignment triad (assignee restricts; else candidate user/group; no triad = open). |
ProcessEvent(ctx, eDef) |
store the synthetic completion event's outputs (already authorized + validated by the loop) for Exec to bind. |
ValidateOutputs(outputs) |
reject a completion missing a required: true output. |
Assignments() / Renderers() / Outputs() |
inspect the declared triad, forms, and expected outputs. |
AddBoundaryEvent(be) / BoundaryEvents() |
attach / inspect boundary events. |
Dehydratable(ctx, re) bool |
reports true — a parked human task lets the whole instance release its goroutines (the task keeps living in the distributor's inbox). |
Behavior worth knowing: a User Task is a wait node. It parks on the same
event channel as catch events — the track goroutine is released, not held —
and resumes only on an authorized, validated Complete. Authorize gates
Take and Complete against the actor identity the distributor supplies:
an assignee is the restrictive gate (only that user); otherwise a matching
candidateUser or an intersecting candidateGroup authorizes; a failed or
empty expression denies. A non-nil error from Authorize is a non-terminal
denial — the task stays parked, waiting for the right actor.
See also¶
- Examples:
examples/usertask/(console-driven approval) - Related guides: Service Task · Human tasks · Process, instance, track, token · Boundary events · Multi-Instance — a User Task over a collection offers one task per item, all at once, each completed on its own
- Design: ADR-020 — Human interaction execution model
- Full API:
go doc github.com/dr-dobermann/gobpm/pkg/model/activities