Forums

Articles
Create
cancel
Showing results for 
Search instead for 
Did you mean: 

Our Forge workflow condition never fired, and forge lint had nothing to say about it

Disclosure: I work on CogniRunner, a Marketplace app for Jira Cloud. This is a platform finding we hit while building it, plus what we changed off the back of it. Everything measurable here was measured, and I have said where the limits are.

We removed a feature from our own app in July because we decided it did nothing. Last week we built a probe app to prove that properly, and found we had been right about the symptom and wrong about the cause. The correct version is more useful than what we had, so here it is.

What a Forge workflow condition actually is

CogniRunner puts AI-evaluated rules on Jira workflow transitions. Validators block a transition, post-functions act after one, and we also shipped a condition. The condition never gated anything, on any surface, and we could not work out why from the code.

The answer is in the manifest reference and it is easy to walk past. As of the 13 January 2026 revision, jira:workflowCondition declares nine properties: key, name, description, expression, projectTypes, configurationDescription, create, edit and view. There is no function, no endpoint and no resolver. A Forge workflow condition is gated by a Jira expression, and expression is required.

Our manifest declared a function alongside it. That function was simply never going to run, because the module has nowhere to run one.

Two caveats before anyone generalises this, because I nearly did. The module is flagged Preview, and that page is dated January 2026, so it can move. And this is specific to conditions: a Forge app absolutely can run its own code on a transition, just as a validator that rejects on submit instead of a condition that hides the button. If you read the paragraph above as "Forge workflow rules are expression-only", that is not what it says and it would be wrong.

Compare it with jira:workflowValidator, which does take a function, and where the docs are explicit that it is one or the other: "The validator requires either the function or the expression in your Forge app. Only one of the two properties must be present in your Forge app." Two neighbouring modules, one takes a lambda and one cannot, and the manifest for both is YAML that looks about the same at a glance.

It is not a Forge quirk either, which I checked before blaming the newer platform. The Connect workflow condition is expression-only in the same way, with the same override-via-configuration behaviour and no remote callback. Conditions have simply never been a place you run your own code.

The part that cost us the time

Nothing told us. The undocumented function key on a condition passes forge lint with no error, and deploys clean. I checked that on CLI 12.21.0, 13.3.0 and 13.4.0.

It is not that lint ignores the module. Take the required expression away and both versions complain immediately. What lint does not do on this module is check for properties it does not recognise, and that is not specific to function. I put a key called totallyBogusKeyThatDoesNotExist in a condition and it passed just as quietly.

So the failure mode is: your manifest is wrong in a way the tooling will never mention, your app deploys, and your rule silently does nothing. If you have ever had a Forge module that "just doesn't fire", check the module reference for the property you are relying on before you debug your own handler for an afternoon. I did it the other way round and it was not a good use of a Tuesday.

What conditions and validators actually do at the API

This is the part I had wrong, and it matters if you are automating against Jira rather than clicking in it.

A condition that evaluates false is enforced server-side. It is not a UI nicety. The transition is left out of the default response of GET /rest/api/3/issue/{key}/transitions, filtered out entirely, not returned with a flag on it. Atlassian's own spec is clear about the default once you go looking: the includeUnavailableTransitions parameter, "whether details of transitions that fail a condition are included in the response", is a boolean defaulting to false. Opt in and the same transition comes back carrying isAvailable: false.

Post the transition id anyway and you get HTTP 400, with a message that tells you very little:

Can't move (KEY). You might not have permission, or the work item is
missing required information. If you keep having this problem, contact
your Jira Administrator.

Nothing is written to the issue's changelog. Jira does not say which condition refused, so that message cannot be used to work out which rule you are fighting.

A validator behaves the opposite way round. It never touches the listing at all, the transition is listed and marked available, and it only blocks when you actually POST. When it blocks, its own errorMessage reaches the caller, so a validator can tell you why and a condition cannot.

That is measured, not inferred: a control set on two self-loop transitions on the same status, on a company-managed workflow on our test site, on 19 August 2026. The condition was absent from the listing and the forced POST 400ed with zero function invocations. The validator was listed, and its POST returned 400 carrying the string my handler had set. I have tested that one REST path and not every surface, so take it as far as that and no further.

There is a way to make a condition dynamic, incidentally, which the same page documents and which we missed the first time: workflowRules.onConfigure can return an expression key that replaces the manifest expression at configuration time. And an expression reading an issue entity property works and fails closed when the property is absent, which is the behaviour you want from a gate.

What we changed

The condition rule type is gone from the wizard. New rules are validators, which run a function and can tell you why they blocked. The module stays in the manifest so existing rules still render and edit, and the code carries a comment explaining why, so whoever reads it next does not find a silent deletion.

Three other things went in this month that are worth mentioning because each came from something breaking rather than from a roadmap.

A premade rule whose type was not in the catalog did nothing and looked like it passed. Fail-open is the right default for a workflow gate, but invisible fail-open is not, so it now logs a warning naming the bad type. We found it by building a rule bed at scale; three rules tested by hand would never have shown it.

Our MCP client had no retry, so a single 502 from a tool under load dropped the whole call and the AI reached its verdict without that tool's data. Reads now retry. Writes still get exactly one attempt, deliberately, because a lost response on a create call must never turn into a duplicate attachment on someone's issue.

And we relicensed from AGPL-3.0 to Apache-2.0. The point of open-sourcing a Forge app is that people can reuse pieces of it, and copyleft works against that for anyone building a Marketplace app. The sweep missed two files, which is its own small lesson: our test fixtures contain non-UTF8 bytes, grep -I decided they were binary and skipped them, and the old headers survived. A binary-inclusive pass caught it.

Two installs

CogniRunner has two installs. It is flagged beta on the Marketplace and it has no reviews. I would rather write that down than have you find it out.

We are shipping updates to it anyway, and the plan is to keep doing that in the open. The work above was driven by Goose, our own agent, which is a fork of block/goose we have been pointing at local models, and we intend to run some of this as live coding sessions on YouTube instead of only writing it up afterwards. If a rule bed of 394 rules or a probe app that settles an argument about a manifest is the kind of thing you would watch someone actually do, that is roughly what those will be.

The honest competitive position: there is at least one other Jira Cloud app selling AI-authored validators and post-functions, and it has considerably more installs than we do. What I have not found elsewhere is the provider spread. CogniRunner will talk to Anthropic, OpenAI, Azure, OpenRouter and Bedrock, to Atlassian's zero-key Forge LLM, and to a self-hosted OpenAI-compatible server, which in our case is LM Studio on a machine in the corner reached over a Tailscale funnel. If your rules are reading issue content and your policy is that the content does not leave your infrastructure, that last one is the whole conversation.

What I would like to know from anyone else building on Forge: have you hit a module where the property you needed was not on the reference and nothing told you? I am now fairly sure conditions are not the only place lint waves an unknown key through, and I would sooner learn the rest from you than one afternoon at a time.

1 comment

Mia Tamm _Simpleasyty_
Atlassian Partner
August 20, 2026

This is a really good write-up, @Gabriela - LeanZero.

The part about forge lint passing while the workflow condition could never actually run is probably the most useful takeaway for me. Those are exactly the bugs that are painful because everything looks valid until you test the real behaviour.

I also liked the distinction you make between conditions and validators at API level. It’s easy to think of them as two flavours of the same thing, but the way Jira exposes them is very different.

The practical lesson here is a strong one: a clean deploy and a clean lint result don’t necessarily mean the manifest describes something Jira can actually execute.

Thanks for documenting the failure as well as the fix — posts like this save other developers a lot of time.

Comment

Log in or Sign up to comment
TAGS
AUG Leaders

Atlassian Community Events