Track network and security delivery as agile work in Versus Desk — projects, issues, a data-driven workflow engine, Scrum and Kanban boards, sprints, and VQL, the Jira-compatible query language that powers every board, filter, and the REST search API.
Versus Desk includes a full agile issue tracker — projects, issues, boards, sprints, and roadmaps — modeled closely on Jira so a team that already works this way feels at home. It exists so that the delivery work around your automation (fleet upgrades, migrations, remediation, certificate lifecycle) can be planned and tracked in the same platform that executes it, and so a board transition can launch a real Versus workflow run.
Everything that lists or filters issues — the issue navigator, board columns, saved filters, quick filters, webhooks, and automation rules — runs through one query language, VQL (Versus Query Language). VQL is Jira-Query-Language-compatible, so queries you already know keep working, and it compiles to safe, parameterized database lookups rather than raw SQL.
A project is the container for issues and carries a short uppercase key (NET, SEC, CLD) that prefixes every issue in it. Projects are created from a template — scrum, kanban, or business — which seeds the right features (backlog, sprints, board, timeline, releases, components, reports), a board, default roles, and two starter quick filters. Each project is either company-managed or team-managed.
When an issue is created, Versus row-locks the project's counter and allocates the next number atomically, producing an immutable key like NET-142. The number is monotonic per project and never reused.
An issue is the atomic unit of work. Issue types carry a hierarchy level that defines the parent/child structure:
| Type | Hierarchy | Role |
|---|---|---|
| Epic | 1 | Groups stories/tasks/bugs via epic_link |
| Story / Task / Bug | 0 | Standard, board-level work |
| Sub-task | -1 | Child of a standard issue via parent |
Beyond the basics (summary, description, priority, assignee, reporter), an issue carries agile fields: story points, sprint, labels, components, a fix version (release), time-tracking estimates, timeline start/due dates, and a rank for ordering. Issues can be linked to one another (blocks, is blocked by, duplicates, relates, clones, causes) and bridged to the ITIL side of Versus Desk — an agile Issue can link to a Change or Incident Ticket so delivery work and operational work stay connected.
Each issue's lifecycle is driven by a data-driven workflow, not hard-coded states. A workflow is a set of status nodes and transitions stored as rows, so it can be edited without code changes. Every status belongs to one of three categories — To Do, In Progress, or Done — which is what boards, reports, and resolution logic key off.
The built-in Software Simplified workflow ships as:
To Do ──Start Progress──▶ In Progress ──Ready for Review──▶ In Review ──Approve──▶ Done
▲ ▲ │ │
└────────── Reopen ──────────┴───────────── Reject ────────────┘ Stop Progress
Transitions can carry conditions (who may run them), validators (checks that must pass before the move — a failure aborts the whole transition atomically and returns 400), and post-functions (side effects applied after the move). Moving an issue into a Done-category status sets its resolution and resolved_at; moving it back out clears them, matching Jira semantics. Every transition is written to an immutable transition log and field-history trail.
A board is a live view of a project's issues, grouped by workflow status into columns. Boards come in two flavors:
sub_filter_jql, e.g. resolved <= -2w) so completed cards age off the board.A board is defined by a small amount of configuration:
| Setting | What it controls |
|---|---|
filter_jql | The VQL scope of the board (e.g. project = NET) |
column_config | Named columns, each mapping to one or more workflow statuses, with an optional WIP max |
swimlane_config | Horizontal grouping: none, assignee, epic, or component |
card_fields | Which fields render on each card |
| Quick filters | Named VQL snippets AND-composed onto the board on click |
Requesting a board's issues returns them grouped swimlane → column, along with assignee facets, quick filters, and per-column insights — issue count, average age, and how many cards have gone stale (no update in three or more days). Dragging a card to another column posts a transition to that column's status; if the workflow has no explicit edge, Versus synthesizes a direct move so a drag never dead-ends.
Backlog and board order is maintained with a LexoRank-style scheme: each issue's rank is a base-36 string, and inserting an issue between two neighbors computes a midpoint string. Reordering is O(1) and never renumbers the cards around it. A rebalance pass can regenerate evenly spaced ranks if strings grow long.
To reorder, post the neighbors you're dropping between:
POST /api/agile/issues/{key}/rank/
{ "before": "NET-18", "after": "NET-42" } // place after NET-18, before NET-42
Sprints belong to a board and move through future → active → closed. A board may have only one active sprint at a time.
POST /api/agile/boards/{id}/sprints/ # create a future sprint
POST /api/agile/sprints/{id}/start/ # { "duration_days": 14, "goal": "…" }
POST /api/agile/sprints/{id}/issues/ # { "issue_keys": ["NET-1","NET-2"] }
POST /api/agile/sprints/{id}/complete/ # { "move_to": "backlog" | "<sprint_id>" }
Starting a sprint stamps its start and end dates (14 days by default). Completing it moves incomplete issues back to the backlog (or forward into another sprint) and records a report — committed vs. completed story points — which becomes a data point in the velocity chart. Closed-sprint reports feed three reports on the project: velocity (committed vs. completed across recent sprints), burndown (ideal vs. remaining points for the active sprint, derived from when each issue entered a Done status), and distribution (counts by category and type).
VQL is the single query language behind the issue navigator, board and quick filters, saved filters, webhook match rules, and automation-rule conditions. One grammar compiles to one safe Django ORM translation — user input only ever reaches parameterized lookups, never string-concatenated SQL.
AND, OR, NOT, and parentheses for grouping.=, !=, ~ (contains), !~ (does not contain), <, >, <=, >=.IN (...), NOT IN (...).IS EMPTY, IS NOT EMPTY (NULL accepted as a synonym).ORDER BY <field> [ASC|DESC], comma-separated.currentUser(), now().-1d, -2w, +3d.| Field | Notes |
|---|---|
project | Project key |
type / issuetype | Issue type name |
status, statusCategory | Status name, or category (todo/in_progress/done) |
assignee, reporter | Username; use with currentUser() |
priority | 1 (highest) … 5 |
resolution | Unresolved matches empty |
summary, description, text | text searches summary and description |
key, labels, component, sprint | |
epic / epicLink, parent | By issue key |
storyPoints, rank | |
created, updated, resolved, due | Dates; use relative offsets |
cf[key] | Any custom field, matched by its key |
project = NET AND status != Done ORDER BY priority ASC, updated DESC
assignee = currentUser() AND sprint IN ("NET Sprint 3") AND resolution = Unresolved
type = Bug AND priority <= 2 AND created >= -7d AND labels IN (regression, panos)
epic = NET-1 AND cf[environment] = Production AND text ~ "BGP"
Custom fields are resolved without a per-field migration: cf[key] compiles to a lookup against the issue's stored field values. Because label and custom-field predicates can multiply rows, VQL de-duplicates those results automatically.
The navigator and every list route through one search endpoint:
POST /api/agile/search/
{ "jql": "project = NET AND status != Done", "startAt": 0, "maxResults": 100 }
200 OK
{ "total": 42, "startAt": 0, "maxResults": 100, "issues": [ … ] }
Validate a query without running it (for live editor feedback) at POST /api/agile/search/validate/, which returns any syntax error, unknown field, or unknown order field with its character position. The plain list endpoint GET /api/agile/issues/?jql=… accepts VQL too.
Isolation is never the query's job. VQL only filters. Tenant and permission scoping are applied to the base result set by the server before VQL runs, so a query can never widen what a caller is allowed to see. See Platform Architecture for how tenant isolation is enforced in the data layer.
This is what ties the tracker to the rest of Versus. A workflow transition can carry a binding to a Versus automation workflow, in one of two modes:
run_gated until the launched run finishes; the run's outcome then drives the terminal move (advance_on_success) or a failure path (on_failure).When the bound run reaches a terminal state, a run → issue callback reads its status and advances the issue, optionally posting the run's result as a comment. In the shipped demo, moving an issue from To Do to In Progress launches a safe multi-section deploy workflow and auto-advances the issue to In Review when the run completes — no device is touched.
Automation rules generalize this to events rather than manual transitions. A rule fires on issue_created, issue_transitioned, field_changed, commented, or scheduled, optionally filtered by a VQL expression, and launches a workflow. Rules are throttled and hash-chain audited — each firing extends a running hash so the history is tamper-evident.
/worklog) and set original/remaining estimates (/estimate); worklogs roll up into time spent and remaining.POST /api/agile/import-jira/ ingests a Jira export JSON to bootstrap projects and issues.