Forward-deployed / AI Knowledge Graph
Flowable

Task queries & the task inbox pattern

Motto — Every worklist UI is two queries wearing pixels: my claimed tasks plus my groups’ unclaimed pool. Get the two-list shape right, and the rest is styling.

Part of Phase 03 — User tasks, identity & forms. This is the phase’s closing Use It lesson.

The Problem

The phase so far gave you tasks with lifecycles, group pools, identity, and form contracts. One question remains: what does Asha see when she opens her work screen? Teams overthink this into “workflow portals” with saved searches and seventeen filters. Or they underthink it into one flat list where claimed and unclaimed work blur together. Then analysts either hoard — claim everything, work none of it — or collide, working unclaimed items without claiming them. That’s lesson 01’s race, reintroduced by the UI.

The Concept

The inbox pattern is two lists with different verbs:

<style> .dgm-tqi{background:var(–color-surface);border:1px solid var(–color-border);border-top:3px solid var(–color-border-danger-outline); border-radius:16px;padding:28px 26px 22px;box-shadow:0 1px 3px rgb(var(–shadow-color) / 0.04),0 8px 24px rgb(var(–shadow-color) / 0.04); margin:26px 0;max-width:100%;font-family:inherit;text-align:left;position:relative} .dgm-tqi-chip{position:absolute;top:-11px;left:24px;background:var(–color-background-danger-solid);color:var(–white); font-size:10.5px;font-weight:700;letter-spacing:1px;text-transform:uppercase; border-radius:20px;padding:3px 12px;box-shadow:0 2px 6px color-mix(in oklab, var(–color-background-danger-solid) 28%, transparent)} .dgm-tqi h4{margin:0 0 4px;font-size:11px;font-weight:700;letter-spacing:.4px;text-transform:uppercase;color:var(–color-text-secondary)} .dgm-tqi-sub{font-size:12px;color:var(–color-text-secondary);margin:0 0 16px} .dgm-tqi-cols{display:grid;grid-template-columns:repeat(2,1fr);gap:12px} .dgm-tqi-col{border-radius:12px;padding:12px 13px;border:1.5px solid} .dgm-tqi-col.neutral{background:var(–color-surface-tertiary);border-color:var(–color-border)} .dgm-tqi-col.accent{background:var(–color-background-danger-surface);border-color:var(–color-border-danger-surface)} .dgm-tqi-col.blue{background:var(–color-background-info-surface);border-color:var(–color-border-info-surface)} .dgm-tqi-col.green{background:var(–color-background-success-surface);border-color:var(–color-border-success-surface)} .dgm-tqi-col h6{margin:0 0 8px;font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.3px} .dgm-tqi-col.neutral h6{color:var(–color-text-secondary)} .dgm-tqi-col.accent h6{color:var(–color-text-danger)} .dgm-tqi-col.blue h6{color:var(–color-text-info)} .dgm-tqi-col.green h6{color:var(–color-text-success)} .dgm-tqi-item{background:var(–color-surface);border:1px solid var(–color-border-info-surface);border-radius:8px;padding:6px 9px;font-size:10px; color:var(–color-text);line-height:1.35;margin-bottom:5px} .dgm-tqi-item:last-child{margin-bottom:0} .dgm-tqi-link{display:flex;justify-content:center;color:var(–color-text-tertiary);font-size:9.5px;font-weight:700; text-transform:uppercase;letter-spacing:.3px;padding:6px 0} @media (max-width:640px){.dgm-tqi{padding:20px 16px 18px}.dgm-tqi-cols{grid-template-columns:1fr}} .dgm-tqi-note{margin-top:10px;padding:9px 14px;background:var(–color-background-danger-surface);border:1px dashed var(–color-border-danger-surface);border-radius:10px; font-size:10.5px;color:var(–color-text-danger);line-height:1.4} </style> <div class=“dgm-tqi”> <span class=“dgm-tqi-chip”>One inbox, two queries</span> <h4>MY TASKS and POOL are the same table, filtered two ways</h4> <p class=“dgm-tqi-sub”>Claiming a pool task is just an UPDATE that sets the assignee.</p> <div class=“dgm-tqi-cols”> <div class=“dgm-tqi-col accent”> <h6>My tasks — assignee = asha</h6> <div class=“dgm-tqi-item”>Verb: FINISH — claimed; nobody else can act</div> </div> <div class=“dgm-tqi-col blue”> <h6>Pool — candidateGroup ∈ asha’s groups, unassigned</h6> <div class=“dgm-tqi-item”>Verb: CLAIM — first claim wins</div> </div> </div> <div class=“dgm-tqi-note”>Claim moves a pool task into My tasks. Complete removes it from both — the token moves on.</div> </div>

Rules the shape encodes:

  1. My-tasks first, always. A claimed task blocks everyone else (lesson 01), so claimed-but-idle work is the most expensive kind. The UI ordering is the policy “finish what you claimed before taking more”.
  2. The pool’s verb is claim, not complete. One click moves it to my tasks, and completion happens there. UIs that offer complete-from-pool re-create the collision the lifecycle exists to prevent.
  3. Groups come from identity claims (lesson 03). The inbox never asks the engine who Asha is — it passes her groups in.
  4. Urgency = due date. dueDate on the task (set in the model or at runtime) drives sort order and the OVERDUE flag. Phase 7’s escalation timers fire off the same field. An inbox sorted by creation date instead of due date quietly optimizes for staleness.

Some query mechanics are worth knowing before you need them. The REST query endpoint (POST /query/tasks) supports pagination (start/size — inboxes over large pools must page), sorting, and variable filters (processVariables: [{name, operation, value}] — for example, “show only applications above ₹10 lakh”). For dashboards that include finished work, the same query shape exists against history (/query/historic-task-instances). Runtime queries only ever see open tasks, because of Phase 2’s delete-on-complete rule.

Use It

code/inbox_client.py is the pattern as a CLI. The merge de-duplicates tasks visible through several of the user’s groups:

def inbox(user, groups):
    mine = query({"assignee": user})
    pool = [t for g in groups for t in query({"candidateGroup": g, "unassigned": True})]
    seen, merged = set(), []                    # de-dupe across shared groups
    ...

Run it against whatever tasks your engine holds from earlier lessons:

$ python3 inbox_client.py asha credit-ops,mumbai-ops
== asha: my tasks (1) — finish these first
  [OVERDUE ] Manual credit review  (task 7512, instance 7501)
== pool for ['credit-ops', 'mumbai-ops'] (2) — claim to take one
  [due 07-25] Verify documents  (task 7688, instance 7640)
  [        ] Collect documents  (task 7710, instance 7699)

Pair it with lesson 02’s claim/complete and lesson 04’s form renderer, and you have a functioning ops console in about 200 lines of stdlib Python. That’s the point: the engine carries the state, and clients stay thin.

Ship It

This lesson ships code/inbox_client.py: the two-query inbox with de-dupe, due-date sort, and overdue flags. The capstone driver reuses it to show pending work mid-run.

Check Yourself

Q1. Why must the pool query filter unassigned: true?

  • A) performance
  • B) claimed tasks belong to one person — showing them in others’ pools invites the collision claiming prevents
  • C) the API requires it
  • D) unassigned tasks have no candidate groups

<details><summary>Answer</summary>B — the pool is by definition the claimable set. Without the filter, Ravi sees (and mentally queues) work Asha already owns.</details>

Q2. A task shows in Asha’s inbox via both credit-ops and mumbai-ops. The inbox should…

  • A) show it twice — it’s in two pools
  • B) show it once — pools are views over one task table; the de-dupe reflects that there is one task
  • C) show it under whichever group is alphabetically first
  • D) error

<details><summary>Answer</summary>B — candidate groups are alternative routes to the same row, not copies of it.</details>

Q3. Yesterday’s completed reviews belong on a dashboard. Which query serves it?

  • A) /query/tasks with state: completed
  • B) the historic task query — runtime tables drop tasks at completion (Phase 2), so finished work lives in history
  • C) /runtime/tasks?includeCompleted=true
  • D) completed tasks are gone forever

<details><summary>Answer</summary>B — the runtime/history split from Phase 2 decides which endpoint can even see the data.</details>

Challenge. Add --variables amount>1000000 filtering to the client, using the query endpoint’s processVariables operations. Then add a --summary mode that prints per-group pool depth and oldest-item age — the two numbers an ops lead actually watches. You’ve built the metrics feed that Phase 9’s dashboard lesson formalizes.

Related