What LeanZero Management does that the other Jira PPM tools don't
Mihai Perdum
Author
18 min readAugust 31, 2026
Key takeaways
A BUFFER TASK holds its end date fixed and shrinks as upstream work slips, then collapses and propagates once consumed. I did not find one documented in BigPicture or Structure.Gantt.
PLAN PROTECTION reverts an out-of-plan start-date edit made in Jira and comments naming the blocker. Others address the same worry earlier: BigPicture blocks the edit, WBS Gantt-Chart silently corrects it.
The dependency rule is the LATEST of each link's lag-adjusted start — not the latest due date plus lag. Those differ whenever lags differ, and the second is wrong.
Apply re-reads Jira and compares against intent. On mismatch it keeps your draft and refuses, rather than re-indexing over what you meant.
It is live on the Marketplace and free today, evidenced by an absent pricing record with a positive control. BigPicture and BigGantt are also free up to 10 users — the difference is above that line.
There are a lot of Gantt charts for Jira. There are considerably fewer scheduling engines, and the difference only shows up on the day something slips.
LeanZero Management is live on the Atlassian Marketplace and free today. This piece is about the parts of it that are not a Gantt chart — the engine rules, the write path, and the way a plan defends itself — and about how those compare with what the established tools actually document. I have tried to be exact about the second part, because a vendor claiming uniqueness is the least trustworthy sentence in software marketing, and the only thing that makes it worth reading is showing my work.
Two housekeeping notes before the substance. The version on the listing is what you install; the repository carries a different number, and the listing wins. And every capability below is traced to a file and a line in the app's own source, because "it does X" from a vendor is worth about as much as the effort behind it.
The buffer task
This is the feature I would keep if I could only keep one.
Flag a task as a buffer and it stops behaving like a task. Its end date is held fixed. When upstream work slips and pushes its required start later, the buffer does not move — it shrinks. Ten working days of protection becomes five, then two. Downstream dates do not change at all, because the thing after the buffer still starts when it always did.
Then, at some point, the slip exceeds what the buffer was holding. The required start passes the fixed end date, and the buffer collapses: duration drops to one day, the end date snaps to the start, and the delay propagates to everything downstream for the first time.
Here is the core of that rule, from static/ppm-ui/src/hooks/cascade-core.js:249-260:
There is an else branch below that block, for a buffer with no due date to hold: a declared duration is kept and the end rebuilt from it, and with neither there is a one-day stub. It exists because three code paths once disagreed about that case.
Three things in that block are worth pointing at.
fixedDue comes from issue._original?.dueDate || issue.dueDate — the last-indexed value from Jira where there is one, so a chain of edits during one planning session cannot quietly walk the anchor forward. Where there is no indexed value it falls back to the current one, which is the only thing it can do for a task Jira has not seen yet.
The compressed duration is measured in working days (workingDaysBetween), so a slip across a weekend consumes the buffer by the working days it actually costs, not by calendar days.
And the collapse is not an error state. duration = 1 with the end date snapped to the start is a legitimate, renderable task that says, visually, there is nothing left here. The plan does not throw. It shows you the day your protection ran out.
That is the whole idea of a buffer in critical-chain planning: you do not pad every task, you pool the padding in one place and then watch it get consumed. A plan where four tasks each quietly carry two days of private padding tells you nothing. A plan with one eight-day buffer that is now down to three tells you exactly how much trouble you are in.
What the other tools do instead
I went looking for this in the four tools a Jira shop would realistically be choosing between: Atlassian's own Advanced Roadmaps, Structure by Tempo with Structure.Gantt, BigPicture and its cheaper sibling BigGantt, and Ricksoft's WBS Gantt-Chart.
BigPicture is the one I can be most precise about, because its documentation is readable and searchable and I checked it myself. It documents five scheduling modes — Auto basic, Auto bottom-up, Auto top-down, Manual and Locked — on its Scheduling mode page. The closest thing to a buffer is Locked, and it is genuinely close: "The duration and position of Locked tasks can't be changed", and their start and end dates are "unaffected by" manual date changes, dependencies, the scheduling mode of other tasks, and a parent being moved on the timeline.
So BigPicture can hold a date fixed. What it does when the schedule pushes against that fixed date is flag it — a parent-task conflict, drawn as a dashed box, telling you the children overrun their parent. It does not compress an already-scheduled task's duration to absorb the conflict, and it does not publish how much reserve is left. (It will shorten a task on creation to fit inside a locked parent — that is a different operation, applied once, not a running reserve.)
That is the real distinction, and it is narrower and more useful than "nobody else can pin a date". Both approaches notice the conflict. One draws a warning around a task that has not changed; the other consumes a measurable reserve and shows you the balance.
For the terms of art, the same documentation is silent: searching that page for buffer, critical chain, CCPM, total float and free float returns nothing, while the mode names it does document return plenty. So the page is loaded and searchable, and the absence is real rather than a failed request. I checked the wider space too, and those five terms do not appear in it.
Structure.Gantt is the other one I can speak to directly. Its Cloud documentation runs to eight pages, and across all of them buffer, critical chain, slack and float do not appear; the only relevant hit is lag, on the dependencies page. My control for that: a nonexistent page on the same site returns a distinctly shorter stub than any real page, so I know I was reading content and not an error.
What Structure.Gantt does have, and it is more than this app does, is genuine resource levelling — with an important boundary stated in its own documentation: "Resource leveling delays items in the Gantt chart only. It DOES NOT reschedule work items in Jira." That is a real capability I am not claiming, and the levelling lives in its own layer rather than in your issues.
Advanced Roadmaps and WBS Gantt-Chart I checked less exhaustively, so treat what follows as scoped to what I read.
The honest, checkable version: I looked for a buffer object — one with its own duration, a fixed end date, compressing behaviour and a readable remaining reserve — and did not find one documented in BigPicture or Structure.Gantt. If you find one, that is a fair correction and I would rather have it than not.
The dependency rule, and the version of it that is wrong
Move a predecessor and its successors move. Every tool in this category does that. The interesting part is what happens with several predecessors and different lags on each link, because there is an intuitive rule that is wrong and it is wrong quietly.
The intuitive version: take the latest predecessor due date, then add the lag. The correct version, and the one the engine implements: compute each link's lag-adjusted required start separately, then take the latest of those.
Those coincide whenever every lag is the same, which is why the wrong rule survives so long in so many spreadsheets. Give them different lags and they diverge.
Take a real calendar, because this is exactly the kind of example that goes wrong in the abstract. Predecessor A is due Friday 9 January 2026 with lag 0, so the successor can start on Monday 12 January. Predecessor B is due Thursday 8 January with a lag of 5 working days, which lands on Friday 16 January. A has the later due date, so the wrong rule gives you 12 January. The right answer is 16 January, because B's lag is a real constraint that has not been satisfied yet.
From cascade-core.js:61-69:
js
1for(const predKey of preds){2const pred = updatedMap.get(predKey);3if(!pred?.dueDate)continue;4const predDue =parseDate(pred.dueDate);5if(!predDue)continue;6const lag = lags ?(lags[predKey]||0):0;7const rs =getRequiredSuccessorStartWithLag(predDue, lag, ctx);8if(rs &&(!required || rs > required)) required = rs;9}
Per link, then max. There is a fixture in the test suite whose entire job is to lock this — a multi-predecessor case where the later lag-adjusted start wins over the later due date — because it is precisely the kind of rule that a well-meaning refactor collapses back into the intuitive form.
One more thing in that loop worth naming: getRequiredSuccessorStartWithLag advances working days. A two-day lag over a weekend is four calendar days. This is not universal in the category. BigPicture's lag-time documentation says "The working and non-working days are both included in the Lag time calculation", which spans the weekend rather than skipping it — though the same page also writes "a lag of one week is shown as +5", so read it carefully before assuming. Neither convention is wrong; they produce different dates, and you should know which one you are getting.
Preview equals Apply, and Apply refuses rather than lies
Most planning tools for Jira draw a proposed schedule and then write it. The gap between those two steps is where trust is won or lost, and it is the part of this app I would defend hardest.
Nothing reaches Jira until you say so. The review dialog lists every intended change — date changes, links created, links removed, rank moves — and every row is individually untickable. Untick a row and that issue is left untouched; there is no bulk accept beyond discarding everything.
When you do apply, the write path does five things in order. It takes a plan lock with a five-minute expiry, so a second editor cannot interleave. It re-checks that none of the affected issues has changed in Jira since you started editing, and stops if any has. It writes in chunks with a pause between issues, to stay inside Atlassian's rate limits rather than discovering them. Before each chunk it consults editmeta for the first issue in that chunk and drops fields that issue cannot accept, which is what stops a custom field that is missing project-wide from 400-ing the whole batch. It is a per-chunk sample, not a per-issue check — an issue later in the chunk with a different edit screen is not separately filtered.
And then it does the part that matters: it re-reads the issues back out of Jira and compares them against what it meant to write.
text
1 * 4. Post-write verification (re-fetch and compare)
On a mismatch — write-resolvers.js:207-219 — it releases the lock, does not re-index, keeps your draft, and returns { success: false, retryable: true, failedKeys }.
That "does not re-index" is the whole point. The tempting behaviour after a partial write is to re-read Jira and call whatever is there the new truth. That is how a planning tool silently overwrites your intent with the consequences of its own half-finished write. Refusing, keeping the draft and naming the failed keys leaves you with the plan you built and an accurate list of what did not land.
A write path that can say "I could not do this" is worth more than one that always reports success, and the difference only ever shows up on a bad day.
The plan defends itself
Here is the second capability I could not find an equivalent for anywhere I looked.
Planning tools share a hole: the plan lives in the app, and Jira is still sitting there letting anyone drag a date on a board. You come back on Monday and the schedule is a fiction, because someone moved a start date on Wednesday without opening the planner.
Turn protection on for a plan — it is on by default when you create one — and that edit does not survive. The app subscribes to Jira's issue-updated event. When a protected issue's start date is moved earlier than its dependencies allow, the app reverts it and posts a comment naming the blocking predecessors, the latest predecessor due date, and the earliest start actually available.
Two details that were true of an earlier version of this code and are not true now, which I mention because we published them ourselves and I would rather correct them here than leave them standing. The revert respects your field mapping — it loads the configured start-date field and falls back to the Jira default only when none is set, with the code commenting "The start-date field is per-instance config, NOT always customfield_10015." And the check is now lag-aware: required start is the same max-over-lag-adjusted-starts rule the cascade uses, "so a legitimately lagged link isn't reverted for landing later than the bare +1 adjacency." An earlier build reverted on bare adjacency and would have fought with your own lags.
One honest limit, and it is a real one: protection watches the start date only. The code looks for a start-date change in the changelog and does nothing when there is none. Move a due date directly in Jira on a protected issue and nothing happens. Worth knowing before you rely on it.
Other tools address the same worry at a different moment, and the difference is worth being precise about rather than claiming nobody else thought of it. BigPicture's Locked mode blocks the change up front — its documentation says the mode "blocks the possibility of changing the duration and position of tasks by automation and other users", and to move a locked task you change its scheduling mode first. Ricksoft's WBS Gantt-Chart takes the opposite approach with an auto-correct that cannot be turned off and rewrites dates the user did not touch. Advanced Roadmaps stages changes for review before committing them, which governs what the plan writes to Jira rather than what Jira does behind the plan's back.
So: prevention, silent correction, and staged commit are all represented. What I did not find elsewhere is this particular shape — the edit is allowed to happen, then reverted after the fact with a comment on the issue explaining why. That is a narrower claim than uniqueness, and it is the one the evidence supports.
It brings the fields Jira does not have
There is no duration field in Jira, and no notion of a buffer. So on first index the app creates them — a PPM Duration number field and a PPM Buffer select — and, if an equivalent already exists, adopts it rather than duplicating it.
I raise this because it is the honest answer to a question a careful admin will ask before installing: why does this thing want manage:jira-configuration?
That is the reason. The app declares eleven scopes, four of them write or admin: write:issue:jira-software, write:jira-work, manage:jira-project and manage:jira-configuration. The configuration scope exists to create two custom fields on first run. I would rather write that down than have someone find it in the permission dialog and wonder what else it is for.
On the same theme: the app makes no third-party network calls. There are no remotes declared in the manifest, no external fetch permissions, and no outbound URLs in the source. It does declare an llm module using Atlassian's first-party Forge LLM. The manifest names the model family (claude); the specific pin, claude-haiku-4-5-20251001, lives in app code. Three features use it: plan review, plan assessment, and a natural-language JQL builder. That AI is off unless an admin turns it on — every AI call passes through a gate whose first line is if (!cfg.enabled) return { enabled: false }, with the default false and monthly and daily caps behind it. So: nothing leaves Atlassian to a third party, and with AI enabled, plan text goes to an Atlassian-hosted model. Both halves of that sentence are true and I would not want to publish either one alone.
Analytics that report and never reschedule
The app computes a real critical path — a forward and backward pass with cycle guards, not a longest-chain approximation — a weighted health score, and per-assignee workload with over-allocation flagged.
What it does not do is resource levelling. It will show you that someone is carrying three overlapping tasks. It will never move one of them to fix that. The source says so in its own comment, and I am quoting it rather than paraphrasing because it is the kind of claim that gets stretched:
"It REPORTS contention (and flags over-allocation); it never reschedules anything. No resource leveling."
This is a genuine gap against part of the field — Structure.Gantt does automatic levelling within its own layer — and it is a deliberate one for now. Levelling that moves other people's work needs to be extremely predictable before it is allowed near a plan somebody is accountable for,.
Working days are a plan-level calendar, not a global assumption
Everything above talks about working days, and that phrase is doing real work, so it is worth saying what it resolves to.
The working-day calculator takes two inputs: a set of day numbers that count as working, and a set of dates that do not. From src/services/working-days/calculator.js:42-45:
Both are configured per plan, not globally. That matters more than it sounds. A programme running across a Gulf client on a Sunday-to-Thursday week and a European delivery team on Monday-to-Friday is two different calendars in the same portfolio, and a single instance-wide setting forces one of them to be wrong. Holidays are the same story — an Irish bank holiday is not a German one, and a schedule that quietly works through it produces dates nobody will hit.
The consequence shows up everywhere else in this piece. A two-day lag is two working days on the successor's calendar. A buffer that shrinks from ten days to five has absorbed five working days of slip. Neither of those numbers means anything without a calendar behind it, and the calendar has to belong to the plan.
The analytics are derivations, and they never write
The app computes a set of read-only signals over the plan. I want to describe them precisely, because "analytics" in this category often means a field the tool writes back into Jira, and these are not that.
The critical path is a real Critical Path Method implementation — a forward pass and a backward pass over the dependency graph, as the source describes it at plan-metrics.js:293, with cycle guards on both passes. It is not the longest-chain approximation a lot of Gantt renderers ship, and the cycle guards exist because real Jira link graphs contain loops nobody intended and a naive traversal hangs on them rather than telling you.
The schedule-risk score is the one I find most useful in practice. Rather than a colour somebody picked in a dropdown, it is derived per issue from four things at once, and the source states both the blend and the constraint:
"Blends baseline slip, dependency-chain depth, deadline proximity, and buffer depletion — a risk signal competitors' manual RAG dropdowns can't produce. READ-ONLY: pure derivation, never writes a field."
I have quoted that comment whole, including the boast in the middle of it, because trimming the self-serving half and then repeating the same point in my own voice would be a cheap trick.
Buffer depletion appearing as a risk input is where the buffer stops being a drawing convention and becomes an input to something. A task with three days of slip behind a buffer that still has eight days left is not the same risk as the identical task behind a buffer with one day left.
Milestone slip and baseline variance are two different things, and they are easy to conflate. Milestone slip is measured against _original — the values as last indexed from Jira, which is to say what this looked like the last time we synced. Baseline variance is measured against a baseline you deliberately set: what we committed to. Those answer different questions, and merging them into one number loses both.
Configuration, and how much of it there is
Five admin tabs: Calculation Engine, Field Mapping, Display, Plan Permissions and Maintenance. Not six — and working days and holidays are not among them, because as described above they belong to the plan rather than the instance.
Field Mapping is the one worth knowing about before you install. The app does not assume your Jira looks like a fresh Jira: start date, duration and buffer are all mapped, which is what allows the protection revert to write back to your start-date field rather than the default one — and, as noted earlier, that mapping is now honoured on the revert path, which it was not in an earlier build.
Live updates, and what "live" honestly means
Plans are collaborative, so the app pushes events — someone took a lock, a draft changed, an apply is progressing. I want to be careful about how strongly to sell that, because "realtime" is a word that gets stretched.
Forge gives you two delivery mechanisms and they are not interchangeable. The source names the constraint in its own header:
"TWO delivery planes (a Forge constraint, not a choice)"
Events raised from a resolver — the things that happen because someone in the app clicked — go out one way. Events raised from a queue consumer, an issue-updated trigger or a scheduled job cannot use that path at all, and go out on a global channel that only a matching global subscriber receives. The client opens both.
The part worth stating plainly: the polling fallback is the proven path, and the global plane is strictly additive on top of it. A failure to establish the global channel never flips the connection status away from the path that works. I would not tell you multi-tab live delivery is guaranteed, because it has not been automatically verified end to end; what I can tell you is that the app is not relying on it to stay correct.
There is a security note attached to that design which I think is the right way to document a platform constraint you had to live with. publishGlobal does not enforce app permission scopes, so anyone subscribed to a plan channel could in principle receive those events. The response was to keep the payloads deliberately boring — plan id, event type, version, issue keys — behind unguessable UUID plan ids, and to write down what would have to change if richer payloads were ever added. It is a smaller claim than "our realtime is secure", and you can check it.
What free means, and what it does not promise
The app is free today. That is a fact about the Marketplace record rather than a marketing position, and it is worth showing how it is established, because "free" is exactly the sort of claim that ages.
The Marketplace API states paymentModel: free for the app, and the live cloud pricing endpoint returns 404 — there is no pricing record. On its own a 404 could just mean a broken request, so the control is to ask the same endpoint about a LeanZero app that is paid: CogniRunner returns 200. The endpoint answers for our apps; the absence for this one is a fact about the app.
For context, because "free" without a comparison is not information — annual commercial cloud pricing for the Standard editions, straight from the same API today (BigPicture also sells a dearer Advanced tier; scheduling and dependencies are not gated behind it):
Note the first column. BigPicture and BigGantt are also free up to ten users, as are a lot of Marketplace apps, because Atlassian's pricing model makes the smallest tier free almost by convention. If you are a team of eight, "free" is not a differentiator and I am not going to pretend it is. The difference starts at the eleventh user.
And the part that matters more than the price: we expect to introduce pricing after the beta period. I am not going to put a date on that, because a dated public pricing commitment is a hostage. The honest version is that it is free while it is in beta, we expect that to change, and if you install it now you should assume a paid tier exists in the future rather than that you have found a permanently free PPM tool.
What it does not do yet
A capabilities post that names no boundaries is an advertisement. Five real ones, in the order a real user would hit them.
No resource levelling. Covered above. It reports contention; it never moves anyone's work.
Protection watches the start date only. A due-date-only edit made directly in Jira on a protected issue is not evaluated.
Apply does not survive closing the tab. The write-back happens while you are there. A background queue that would let you start an apply and walk away was deliberately deferred rather than half-built.
Very large plans are not resumable mid-index. Indexing runs in a background job with a fifteen-minute ceiling. A plan that needs more than one pass is not currently resumed from where it stopped.
One dependency type, and no leads. Predecessors come from a single configured link type — finish-to-start only. There is no start-to-start, finish-to-finish or start-to-finish, and negative lag is refused outright: the resolver returns "Lead (negative lag) is not supported yet". For a piece that spends a section on lag semantics, that is the first thing a competitor would point at, so it belongs here rather than in a support ticket.
Protection has a blind spot the source labels itself. The trigger pre-filters changelog entries by field name before loading any configuration, and the code carries its own note: a custom start field "whose display name contains neither 'start date' nor 'due date' AND whose id differs from the default is not recognised here". The configured id is honoured once config loads — but if your start field is named something exotic, the pre-filter drops the event and protection never fires at all. I am stating that in the same piece where I corrected an earlier field-mapping claim, because the correction would otherwise read as a clean bill of health and it is not one.
The browser engine is the authoritative one. There are two scheduling engines — one in the browser for live preview, one on the server for triggers — and they are locked together by a parity harness. There is a known asymmetry: the server-side settle rolls up subtask children but not epic-to-child. The browser engine is the one whose output is written, and that is the one to trust.
[[takeaways]] The short version. A buffer task that holds its end date, compresses as slip arrives, reports what is left and collapses loudly when it is gone — I could not find that documented in the tools I was able to search, and BigPicture's nearest equivalent flags the conflict rather than absorbing it. Plan protection that lets the out-of-plan start-date edit happen and then reverts it with a comment explaining why — where BigPicture blocks it up front and WBS Gantt-Chart corrects it silently. A dependency rule that takes the latest of each link's lag-adjusted start rather than the latest due date plus lag, which are different numbers whenever your lags differ. And a write path that re-reads Jira, compares against what it meant, and refuses while keeping your draft rather than re-indexing over your intent.
It is on the Marketplace, Jira Cloud only, free while it is in beta. The portfolio page carries the full documentation, and if you want the engineering rather than the product, the two-engine parity harness is the more interesting read.
If you try it and something in the list above turns out to be wrong — particularly anything I said about another vendor's product — tell me and I will correct it in place, the way we corrected the field-mapping claim in this one.