Until 13 August 2026 CogniRunner's workflow conditions declared expression: "true" and gated nothing. 3.1.0 replaces that constant with ten deterministic checks.
A Forge condition is a Jira expression, not your code. It has no network, no app storage and no await, so an AI-powered condition cannot exist in any app.
I ran the shipped expression through Jira's own evaluation API against two live issues. Every fail-open path held, and the field checks gate in both directions.
The custom-field-only guard is load-bearing: dynamically indexing an issue with a system field's REST id reads null instead of erroring, which hides the transition on exactly the issues that satisfy the rule.
An expression that cannot evaluate returns false, which hides the transition. In a product whose law is fail-open, the condition module is the one place that inverts.
You can evaluate any app's shipped condition expression yourself against a live issue with POST /rest/api/3/expression/evaluate, binding config through a one-element list and a lambda.
Two installs and zero reviews on the Marketplace as of 24 August 2026. We are shipping anyway, and this post says why.
CogniRunner 3.1.0 went public on the Atlassian Marketplace on 13 August 2026, and the headline is a repair rather than a feature: workflow conditions now gate a transition. Until that release they did not. The module shipped with expression: "true", a constant, and Jira dutifully evaluated it to true on every issue, on every transition, on every surface, for every version from the first one onward.
Our own launch post from March said, in four words, "Conditions hide the transition entirely." That sentence was wrong when it was published. If you installed CogniRunner on the strength of it and configured a condition, the condition did nothing, and nothing in the product told you so. That is the thing to say first, before any of the rest of it.
This post covers what 3.1.0 actually changed, what I measured against a live Jira Cloud instance rather than read off a changelog, what the app does that I could not find another Marketplace app doing, and the parts where the honest answer is that it does not.
Why a condition that "ran" never ran
A jira:workflowValidator and a jira:workflowCondition look like siblings in a Forge manifest. They are not. The validator hands Jira the name of a function; Jira invokes your Node runtime, waits up to 25 seconds, and takes your answer. The condition hands Jira a Jira expression, and Jira evaluates it itself, inside its own sandbox, with no invocation of your app at all.
Atlassian's module reference lists four required properties for the condition module: key, name, description and expression. There is no function property. Ours declared one anyway, for a very long time, and Jira ignored it silently. forge lint passed. forge deploy passed. The lambda was never called, because there was no mechanism by which it could be.
That single illegal key is why the app's own code comments, the admin wizard's help text and our internal test notes all believed a function was involved. We had written a validator and told ourselves it was also a condition. If you want the platform mechanics laid out properly rather than as an apology, we wrote them up separately in jira:workflowCondition vs jira:workflowValidator, which is the piece I would read first if you are building either module yourself.
The second half of the failure is more interesting than the first, because it is the shape of mistake that survives testing. When we finally tested a condition, we observed that a matching issue and a non-matching issue both showed the transition, that forcing it over REST returned 204 for both, and that our lambda had been invoked zero times. From that we concluded that Jira does not evaluate Forge conditions on the REST transition path, wrote it down as a platform limitation, and repeated it in roughly sixteen places across the repo and the listing.
Every observation was real. The conclusion was invented. A constant-true expression passes on every surface, so the test could not have distinguished "REST bypasses conditions" from "this condition is a constant". It proved nothing, and we filed it as proof. The regression suite now covers every condition type in both directions, on the transition listing and on a forced POST /transitions, and Jira enforces conditions on both.
Warning
If you have ever concluded that a platform does not do something because your test showed it not happening, check that your test could have shown it happening. A rule that always passes and a platform that never runs the rule produce identical evidence.
What a Jira expression can and cannot be
This matters for anyone comparing workflow apps, so it is worth stating plainly rather than as a limitation buried at the end.
A Jira expression runs inside Jira. It has no outbound HTTP, no access to app storage, and no await. There is therefore no route by which any Forge app can call a language model from a workflow condition. Not ours, not anyone's. An "AI condition" is not a feature we have not built yet; it is structurally impossible on the platform, and any listing that implies otherwise is describing something else.
CogniRunner's conditions are consequently deterministic and always will be. That turns out to be a better product than the thing we thought we were selling. A deterministic condition costs nothing per transition, adds no model latency, cannot fail open because a provider had an outage, and Jira applies it everywhere it applies conditions: the issue view, REST, automation and bulk changes.
There is one more consequence, and it runs the other way. Two of the checks that ship as conditions cannot be built as validators at all, because Forge does not pass the acting user to a validator function. Jira's expression engine does supply a user binding. So "current user is the assignee" and "current user is the reporter" work as conditions and only as conditions.
The ten checks, and the six that are greyed out
3.1.0 puts conditions back in the Add Rule wizard with ten expression-backed types:
Check
What it reads
Issue type is
the issue's type name
Issue is resolved
whether a resolution is set
Resolution is
the resolution name
Priority is
the priority name
Parent status is
the parent issue's status name
Current user is assignee
the acting user against the assignee
Current user is reporter
the acting user against the reporter
Field has a value
a custom field, null check
Field is empty
a custom field, null check
Field equals
a custom field, typed compare, case-insensitive
11 rows × 2 columnsHeader row enabled
The catalog behind the wizard actually holds sixteen condition entries. Six of them are shown greyed with the reason attached, because Jira's expression sandbox cannot reach what they need: attachments, linked issues, sub-task resolution state, and group or role membership. The picker refuses them rather than accepting a rule that would silently do nothing, which is the failure mode this whole release exists to eliminate. Two of the six have near-equivalents in the validator catalog, which is a separate list: attachment required, and sub-tasks resolved. The other four do not, so the greyed-out message telling you to use a validator instead is more optimistic than the catalog behind it. A linked-issue or group-membership gate has no premade rule on either side of the app today.
Alongside them sit eleven premade validators covering the structural checks people actually configure most: required field, field changed, field comparison, regex, allowed values, text length, date relative to now, sub-tasks resolved, attachment required, comment required, and field cardinality. None of those call a model either.
The field checks nearly shipped in a way that would have been much worse
The three custom-field checks were built, then withdrawn, then rebuilt. The withdrawal is the part worth publishing, because the failure mode is the one that would have cost an administrator a working day.
CogniRunner's runtime law everywhere else is fail-open. A validator that cannot reach its model lets the transition through. A field-based condition can fail the other way. Per Atlassian's documentation, a condition whose expression fails to evaluate or returns a non-Boolean evaluates to false, which hides the transition for everyone on that workflow. A condition that errors does not degrade to permissive; it degrades to a locked workflow.
Two ways to get there. The first is types. Field values arrive typed, and Jira's == is strict. The second is names, and it is the one that would have bitten in production.
Jira expressions name an issue's system fields differently from the REST field ids that a field picker hands you. The expression accessor is issue.dueDate. The REST id is duedate. Index the issue with the REST id and you do not get an error you can catch. You get null.
I did not take that from our own notes. I ran it on 24 August 2026 against a live Jira Cloud instance, on an issue whose due date is genuinely set. p here stands for the field id the rule saved:
text
1on LZPT-230, whose due date is really set to 2026-05-15:
23 issue.dueDate -> "2026-05-15"
4 issue?.[p] with p = "dueDate" -> "2026-05-15"
5 issue?.[p] with p = "duedate" -> null
6 "field has a value" using the REST id -> false
7 "field has a value" using the accessor -> true
Read the line that returns false as an administrator rather than as a developer. You configure "Due date has a value" on a transition. Your field picker gives you duedate, because that is the field's id. Every issue that has a due date now reads null, the condition is false, and the transition disappears from exactly the issues that satisfy your rule. The ones with no due date behave normally. It is the most confusing possible shape of bug: correct data is punished, incomplete data is not.
There is a subtlety underneath it that I only found by testing both forms. If you write the property name as a literal, Jira's expression type-checker catches you:
text
1issue["duedate"]
2 -> Evaluation failed: Unrecognized property of `issue`: "duedate"
If you index dynamically, from a value in the rule's configuration, the type-checker has nothing to check and you get a silent null. Every real app indexes dynamically, because the field is chosen by the person configuring the rule. The safety net exists only in the form nobody ships.
3.1.0's answer is a guard in the expression itself. The property name is only ever used if it matches ^customfield_[0-9]+$. For a custom field the REST id is the expression accessor, so the mismatch class cannot occur. Anything else, including a hand-crafted system id posted over REST, falls through to true and the transition stays visible. System fields stay excluded until each one is verified individually, which has not happened yet.
The typed half was settled the same way, with a 41-case live probe on 13 August that pinned, per field kind, what an unset field reads as, what shape a set value arrives in, and whether toLowerCase() exists on it. The result is nineteen custom field kinds supported for has-value and is-empty, and six of them for equals: text, URL, date, number, select and radio. Rich text is not a String in an expression, so textarea equals is out. Datetime equals is out for the same class of reason. Each supported comparison runs only behind the kind gate its probe cleared.
What I measured against the shipped expression
The manifest expression is 2,119 characters of nested ternaries. Reading it is not the same as knowing what it does, so I ran it.
Jira exposes its own expression evaluator over REST, which means you can evaluate the exact expression an app ships, against a real issue, without deploying anything. One trap first: POST /rest/api/3/expression/eval has been removed and now returns a migration notice. POST /rest/api/3/expression/evaluate is the one that still works.
The second trap is that the endpoint gives you issue, user and project, but not the config object Jira injects when it evaluates a workflow condition. You can bind it yourself with a single-element list and a lambda, and object literal keys must be unquoted:
python
1# The condition config is not a context variable you can declare, but a lambda2# parameter binds it perfectly well.3expr =f'[{config_literal}].map(config => {SHIPPED_EXPRESSION})[0]'45body ={"expression": expr,"context":{"issue":{"key":"COGTEST-2612"}}}6requests.post(f"{BASE}/rest/api/3/expression/evaluate", json=body, auth=(EMAIL, TOKEN))
I pointed that at two issues on our test site. One has every custom field kind populated; the other is identical and completely bare. Twenty configurations, each evaluated against both. shown means the expression returned true and the transition stays visible; HIDDEN means false.
Condition config
issue WITH values
issue with NONE
a condition saved before 3.1.0 (AI prompt)
shown
shown
config missing entirely
shown
shown
rule marked disabled
shown
shown
field-has-value (text)
shown
HIDDEN
field-empty (text)
HIDDEN
shown
field-equals text, exact
shown
shown
field-equals text, wrong case
shown
shown
field-equals text, different value
HIDDEN
shown
field-equals select option "Low"
shown
shown
field-equals select option "High"
HIDDEN
shown
field-equals number = 7
shown
shown
field-equals number = 8
HIDDEN
shown
field-equals number, valueNum missing
shown
shown
system field id smuggled in via REST
shown
shown
exprKind lies: "str" on a number field
ERROR, hidden
shown
exprKind unknown
shown
shown
current-user-is-reporter
shown
shown
issue-is-resolved
HIDDEN
HIDDEN
priority-is Medium
shown
shown
priority-is Highest
HIDDEN
HIDDEN
21 rows × 3 columnsHeader row enabled
Four things in that table are load-bearing.
Every condition you saved before 3.1.0 still allows everything. That is deliberate. The expression only evaluates a config that carries the marker the new wizard writes, so an upgrade cannot change the behaviour of an existing rule from "allows everything" to "hides a transition" on a Monday morning without anyone touching it. Opening an old condition shows you the AI prompt it was saved with, explains that it never ran, and offers to convert it.
The whole right-hand column is shown for every equals check. An empty field never hides an equals check. This is not laziness. A field hidden by a field configuration reads null regardless of what is stored in it, so treating null as "does not equal" would let a routine field-configuration change hide transitions across a tenant. If you want empty to be excluded, you combine it with "Field has a value" and say so explicitly. Has-value's null-hides behaviour is its actual semantics, and it is stated in the rule's own help text in the wizard.
The system field row is the guard from the previous section, working. A rule attached over the REST API with exprProp: "duedate" falls open instead of hiding transitions.
And one row genuinely fails closed. If you attach a rule over REST whose declared kind does not match the field's real kind, comparing a Number to a String is an evaluation error and the transition is hidden. That is a deliberate-misuse surface rather than a path the wizard can produce, and it is documented in the app's REST guide, but it is real and you should know it exists before you bulk-provision rules from a script.
What a condition costs
Nothing in AI terms, which is the point. The interesting question is what the expression itself costs, since it is 2,119 characters of branching that Jira evaluates on every transition listing.
I measured it as a paired difference against the constant true, interleaved so that network drift hits both sides equally, sixty pairs, same issue, same endpoint:
Expression
median
min
constant true
199.9 ms
175.1 ms
CogniRunner 3.1.0 expression
240.2 ms
199.4 ms
paired delta, n=60
+37.6 ms
+24.3 ms min-to-min
4 rows × 3 columnsHeader row enabled
Be careful with that number and I will be careful with it too. It is measured over the public REST evaluation endpoint from a Mac Studio outside Atlassian's network, and the second request also ships two kilobytes more body. So roughly 25 to 38 milliseconds is an upper bound on what Jira spends evaluating the condition, not a measurement of the in-workflow cost, which I have no way to instrument from outside. What it does establish is the order of magnitude. Against an AI validator, which has to reach a model and back inside a 25-second budget, a condition is free.
The rest of 3.1.0, which is about being able to undo things
Conditions are the headline, but the release started somewhere less glamorous. CogniRunner could attach far more rules to Jira than it could administer, and past that line there was no way back.
Delete existed in the backend with no caller anywhere in the interface, and even reached directly it removed only the registry row. The rule stayed attached to its transition and kept executing, now with no interface left to disable it. 3.1.0 adds per-row and bulk delete that detaches the rule from the Jira workflow so it actually stops running, plus a second option, clearly labelled, that removes it from your list and tells you the rule keeps running. The dialog also warns before a trap that is easy to walk into: a rule's disabled flag lives on the registry row, so removing only the row re-enables a disabled rule. It tells you how many of your selection that applies to, and a dry run reports per rule whether it can still be located on its workflow.
The registry is one Forge KVS value with a hard 240 KiB platform ceiling, no eviction, and a row cap of 500. The Rules tab now shows real pressure in bytes against the true capacity, computed site-wide rather than from whatever your filter happens to show. Import checks the cap, which it previously did not, so at the limit it can no longer attach a live workflow rule and then fail to register it. Register-all reports what it skipped instead of discarding its own result and looking like a clean success.
Ownership got fixed in the same pass, and the cause is worth naming: ownerless rows matched every user, and Register-all stamped whoever clicked it as the author of every rule it claimed. Claiming a rule is not authoring it. Claimed rules are now recorded as unowned with a separate record of who claimed them, and a one-time repair un-attributes rules that were already mis-stamped.
Finally, 3.1.0 documents attaching rules over Jira's own workflow REST API, with the extension ARI shape, the payload per rule type, the caps, and the failure modes. There is no CogniRunner API involved; you are calling POST /rest/api/3/workflows/update with your own admin credentials, which is how you would bulk-provision across projects, move rules between sites, or keep them in version control. The in-product panel shows your installation's own ARIs, because the environment id differs between your production and development installs and copying one from an example is a good way to attach a rule that points at nothing.
How this compares to what else is on the Marketplace
The honest comparison has two halves, and CogniRunner loses the first one comprehensively.
If what you want is deterministic workflow extensions, the incumbents are enormous and mature. Reading the Marketplace API on 24 August 2026: Jira Misc Workflow Extensions sits at 16,827 installs, JSU Automation Suite at 12,260, Jira Workflow Toolbox at 4,835. They have had a decade of edge cases reported by customers who were not the vendor. If your requirement is "block the transition unless this field is filled in", buy one of those, or use Jira's native validators, and do not think about it again.
The second half is where the app has a reason to exist. CogniRunner's validators send the field's content, or the content of an attached document, to a language model and block the transition on what the model reads, with the reasoning shown to the person who got stopped. Not "the description is at least 200 characters" but "the description contains steps to reproduce, expected behaviour and actual behaviour". Not "an attachment exists" but "the attached mockup is a UI screenshot and not a stock photo". That is a different category of check, and structural rules cannot express it.
On that half, the nearest neighbour I found is Laya WorkflowOS, at 529 installs, which does build workflow rules in plain English and lists validators and post-functions. Its own listing says it "runs on Atlassian with no external backend", which means Atlassian's AI, and no choice of model. Smart AI for Jira, at 215, does offer LiteLLM with Ollama so your data stays local, but for prompts, summaries and work breakdown rather than workflow rules. ATLAI, at 259, is a governed AI layer over triage and routing. Several of the larger AI-for-Jira apps are assistants, and an assistant is not a gate.
So the sentence I am prepared to defend, having checked rather than assumed, is narrower than a marketing line and more useful: I could not find another Jira Cloud app where an AI check that blocks a workflow transition runs against a model you choose, including one running on hardware you own, in an app whose entire source you can read.
The model choice is real rather than a checkbox. Seven providers: OpenAI, Azure OpenAI, Anthropic, OpenRouter, AWS Bedrock, LM Studio, and Atlassian's own Forge LLM. Five want a credential of yours, the Atlassian one wants nothing, and the LM Studio one wants a URL, which has to be HTTPS on the default port and on a *.ts.net Tailscale Funnel host, because Forge egress is allowlisted in the manifest and will not dial a plain-HTTP endpoint or a non-standard port. Point it at your own machine and the field content your validators read never reaches a third-party model vendor.
The Atlassian provider is the odd one out and the reader should know how it is paid for. It needs no key from you because Atlassian bills Forge LLM tokens to the app vendor rather than to your org, which is us. That is also why it is clamped to Claude Haiku, and why a usage tier is the obvious next thing to build on that path. Every other provider bills you directly and has no ceiling from us at all.
And the code is Apache-2.0, on GitHub, including the manifest expression at the top of this post, the 41-case probe script, and the findings file where the wrong diagnosis is preserved with the evidence that killed it. You can read what the app sends to a model before you let it read your issues. For an app category whose entire proposition is "let this thing look at your data", that seems like the minimum.
goose is our own agent, a fork of block/goose that runs a local-model swarm, and it is increasingly what the work around here is done with. Its connection to CogniRunner is concrete and narrow, so I will keep it to what is true: the swarm dispatches to LM Studio, and CogniRunner's LM Studio provider dials the same server. The model that reviews a plan for goose and the model that decides whether a bug report has steps to reproduce can be the same weights on the same machine, which is not an integration so much as a consequence of both things speaking the same local API.
The forward half is an intention rather than a shipped feature, and I will label it as one. We plan to run CogniRunner live in vibe-coding sessions on YouTube with goose driving, configuring rules against a real Jira and letting the failures happen on camera. There is no date and nothing to sign up to yet.
The vendor statement, because you will look it up anyway
CogniRunner has two installs and zero reviews. Those are the numbers the Marketplace API returned on 24 August 2026, and I am not aware of a customer running it in production.
I would rather write that sentence than have you find it. It changes what you should trust in this post. The regression suite runs against a real Jira Cloud instance rather than a mock, which is a discipline we wrote up in running real end-to-end UI tests against a deployed Forge app, and the numbers above are ones I produced today. So the mechanics are tested. What is not tested is the thing only customers test: the field configuration nobody anticipated, the workflow shared by nine projects, the administrator who does the reasonable thing that the wizard did not consider. Those bugs are still in there and I have no way to find them from here.
We are shipping anyway, and the reason is in this post rather than beside it. The conditions defect was found by attacking our own product rather than by a support ticket, and the fix went through an adversarial review that raised 22 findings and confirmed 7 after independent refutation. Five of those seven were the field-based condition mistake, caught before release rather than after. An app with no users is the cheapest possible place to be wrong, and we would rather spend that window on the failure modes than on a launch.
What it will not do
Conditions are deterministic, permanently, because Jira evaluates them and Jira expressions cannot reach a model. If you want semantic gating you want a validator, and a validator shows an error rather than hiding the button.
Field conditions cover custom fields only. System fields are excluded until each is individually verified. Equals covers six field kinds; rich text and datetime are excluded because their values are not what an expression comparison expects.
A field hidden by a field configuration reads as null, so a has-value condition will hide the transition on issues whose field is populated but not visible in that context. A renamed select option stops matching until you edit the rule, which is the same class of fragility as Jira's own priority and resolution conditions. And a rule attached over REST whose declared kind does not match the field's real kind fails closed on that transition.
3.1.0 is Cloud only, flagged beta in the Marketplace version record, and paid through Atlassian: free up to 10 users, and $75 a year at the 15-user tier as of today's pricing. Auto-update is off for it. The Marketplace version record reports auto-update as not allowed for both 3.0.0 and 3.1.0, where the 2.x releases had it on, so an existing installation has to approve the upgrade rather than receiving it silently.
If you configured a CogniRunner condition before 13 August and it never seemed to do anything, that was not you. Open it in the wizard, and it will now tell you what it saved and what it never ran. What I would like to know from anyone who does that is which check you were reaching for, because the six greyed-out ones are greyed for a platform reason rather than a permanent one, and the order we verify system fields in should be decided by what people actually tried to configure.