
You wire up the obvious thing. LangChain has a Jira toolkit, your model can write JQL, and in an afternoon you have an agent that reads a bug, finds likely duplicates, sets severity, comments, and transitions the issue. It works in the demo. Then you point it at real engineers and it triages the wrong queue, and every comment it posts is authored by a service account nobody recognizes. That is not a prompt problem. It is an identity problem, and it is baked into the auth model the toolkit quietly picked for you.
Stack: LangChain (create_agent) and the Scalekit Jira connector, Python. Adapt the Scalekit LangChain sample scaffold and have it running in under 30 minutes.
The agent takes one incoming bug and runs a fixed sequence. It reads the bug's summary and description, searches open bugs in the same project for candidates, and adjudicates: if the bug duplicates an existing one, it links them as a Duplicate, comments, and transitions the bug to a closed state; if it is new, it assigns a severity, records the rationale as a comment, and transitions the bug to a triaged state. Optionally it files a linked follow-up task for the fix.
Two design commitments matter. The pipeline is deterministic with a single bounded adjudication node, not an autonomous agent free to roam the Jira API. And every action runs as the engineer the work belongs to, not as the agent. The first commitment is a LangChain concern. The second is entirely an auth concern, and it is where the obvious build fails.
Here is the setup almost everyone writes first. It is real, current LangChain, and it runs.
The failure is silent, which is what makes it dangerous. currentUser() in JQL resolves to whichever principal the request authenticates as. Here that principal is the JIRA_API_TOKEN owner, a service account, so assignee = currentUser() returns the service account's queue and the agent triages nothing an engineer actually owns. When it does write, jira_issue_comment_add and the transition both record the service account as the actor. Three months later a compliance reviewer asks who moved PAY-1421 to Done, and the audit trail says the bot. Nobody can answer who was really accountable.
You cannot fix this with a better prompt. The identity is fixed at the credential layer, one token, one account, for every engineer the agent serves. This is a core example of how tool calling authentication for AI agents goes wrong when the auth model doesn't match the identity model.
The toolkit did not choose a static token by accident; that is the only thing JiraAPIWrapper supports. The distinction that matters for an agent is not token versus OAuth in the abstract, it is whether the agent can act as each engineer.
For a single-user personal script the static token is fine. For an agent that triages bugs on behalf of a team, it is the wrong foundation: it collapses attribution, over-privileges the agent to the token owner's full access, and, since Atlassian moved all API tokens to mandatory expiry, it silently dies on a schedule you did not set. What the engineer can do, the agent should do, and no more. A shared token cannot express that. Understanding credential ownership across agent tool-calling patterns is essential before you ship to production.
Triage does not need an autonomous agent that can call any of the connector's Jira tools. It needs a fixed pipeline: gather context, adjudicate once, act within a known set of operations. Giving the model the connector's full catalog would hurt it twice, once on accuracy (more tools in context means worse selection and hallucinated parameters) and once on cost (every tool in context burns tokens before the agent does any work). So the agent is allowed exactly the tools the pipeline uses.
Duplicate detection is the one step worth being precise about. Jira has no native semantic dedupe, so the pipeline fetches candidates with a JQL text search over open bugs in the same project, then the adjudication node compares the incoming bug against that candidate set and decides. Linking is directional: Jira's Duplicate type reads as the new bug "duplicates" the canonical issue, so the new bug is the outward side and the canonical issue is the inward side. For a large backlog, replace the JQL text search with an embedding lookup over issue summaries and descriptions; the pipeline shape does not change, only the candidate retrieval does.
The fix is to make the agent act as each engineer, and to do it without hand-rolling OAuth (3LO), token storage, and refresh per engineer. The Scalekit Jira connector holds one connection for your app and one connected account per engineer, keyed by an identifier you choose. At call time it resolves that engineer's token, so assignee = currentUser() and every write resolve to the engineer.
The sequence is discovery, then scope, then execution. First you retrieve the Jira tools authorized for the current engineer's connected account as native LangChain tools. Then you reduce them to the triage surface. Then you hand that surface to create_agent. This is the same pattern described in LangChain tool calling: how it works, where it stops, and how Scalekit completes it.
The identifier is the whole story. It is the difference between one shared token and per-user identity, and it is the only line of code that changes as you move from a demo to a multi-user agent.
Run the pipeline once per bug, each run bound to the engineer who owns it. Streaming makes every tool call visible, and every one of those calls executes under that engineer's Jira identity.
Compare this to the naive build. The pipeline logic is identical; the toolkit and identity underneath it are not. ana's run triages ana's bug as ana, ravi's as ravi, across two different projects, with no shared credential in the loop. assignee = currentUser() finally means what the model assumed it meant.
Per-user identity fixes attribution, but it introduces the obligation every multi-tenant agent carries. Forty engineers across eight orgs is forty connected accounts to store encrypted, forty OAuth tokens to refresh before they expire, and forty to revoke the day someone leaves. The static-token build hid this obligation behind a single credential; it did not remove it, it made it silently worse, because that one token was over-privileged and shared.
Two failure modes are specific to agents and worth naming.
Scalekit's Jira connector holds the per-user tokens, refreshes them against Atlassian's expiry, and resolves them at call time from the identifier, so the triage pipeline never sees a raw token and the credential lifecycle is not your code to maintain. This is the same challenge covered in depth for how to handle token refresh for AI agents. The question of who revokes an employee's AI agent access when they leave is a real operational problem this architecture solves by design.
Watch four things once real bugs flow through it. Token expiry: Atlassian now force-expires API tokens, so any static-token fallback dies on a timer; per-user OAuth with managed refresh is the path that survives. JQL pagination and rate limits: jira_issues_search is paginated and Jira rate-limits by tier, so a wide duplicate search over a large backlog needs cursoring and backoff, not one unbounded call. currentUser() under shared identity: the moment any part of the system falls back to a shared token, the queue and the attribution are silently wrong again. Silent empty results: an under-scoped or unauthorized read returns nothing rather than an error, so confirm each engineer's connected account is ACTIVE before assuming the agent has access.
These production concerns mirror what teams encounter when moving from single-tenant to multi-tenant tool-calling agent auth — the surface area of failure expands significantly with each additional user identity in the system.
Configure the Jira connection in the Scalekit dashboard, set your SCALEKIT_ENV_URL, SCALEKIT_CLIENT_ID, and SCALEKIT_CLIENT_SECRET, and confirm the connection name in code matches the dashboard exactly. Swap the triage_queue for your real engineers and project keys, run each engineer through consent once, and watch the streamed tool calls resolve as that engineer. To scale duplicate detection, replace the JQL candidate search with an embedding lookup over issue summaries and descriptions; the pipeline and the auth layer stay the same. To write the priority field directly instead of via a comment, extend TRIAGE_TOOLS with the connector's issue-update tool and add it to the policy.
You can, and it will demo cleanly, but it reintroduces the exact failure this build removes. A shared identity collapses assignee = currentUser() to the bot's queue, authors every comment and transition as the service account, over-privileges the agent to that account's full permissions, and now expires on Atlassian's mandatory token timer. Per-user connected accounts keep every action attributed to the engineer.
No. It returns the connector's Jira tools as native LangChain tools; per-user enforcement happens at execution, when the call runs under that engineer's resolved OAuth token. You reduce the tool surface yourself by binding a subset (here, seven tools), which is the accuracy and cost lever.
Jira's Duplicate link type is directional. The new bug "duplicates" the canonical issue, so pass the new bug as outward_issue_key and the canonical issue as inward_issue_key to jira_issue_link_create. Reversing them inverts the relationship in the issue view.
Revoke their connected account and the agent immediately loses that engineer's Jira access. A static API token stored locally would keep working after the person is disabled in your IdP, and native Jira webhooks do not emit user lifecycle events, so token-based setups keep acting until someone manually finds and kills the credential.
No. This build uses direct tool calling with native LangChain tools. If you standardize on MCP, the same Jira tools can be exposed over a Scalekit-generated MCP endpoint and consumed with langchain-mcp-adapters, but it is not required for the pipeline shown here.