Case study
Jira to Linear, built by an agent
Eleven minutes from prompt to the last of six test runs, measured on 2026-09-02 on the owner's own tenant. One session, not a benchmark: your API, your prompt and your agent decide. The connections, credentials and the webhook endpoint existed before the prompt.
The setting
Tenant Demo, sandbox environment. Entered by a person in the browser before the prompt: the JIRA connection; the Linear connection; the jira webhook endpoint; two configured calls: JiraGetIssue, LinearGetIssues. The agent's token was minted for this environment with the credential facet credentials off, so it could neither create nor repoint a connection; write probes were off.
The prompt
Client: Claude Code with the a9script authoring skill, over the MCP authoring surface.
Build a JIRA to Linear Integration. There are already connections to JIRA and Linear, and a configured Endpoint for JIRA webhooks. Whenever there is a new Issue in JIRA, a Linear Ticket should be created. A new comment should be written to the right Linear Ticket. If there is a status change in Jira just write it as a comment to linear. Every hour check the linear issues if they are still open. If they are completed already, write a comment to the corresponding JIRA Issue and clear the connection - further updates from JIRA will not longer be transfered to Linear. Do it in a separate automation called JIRA2Linear and prefix all new entities you use with "J2L". Use as much config functions as possible. Do not change existing scripts or functions. Build it on its own just by using the existing connections.
What the agent did
Transcribed from the owner’s screenshots of the session on 2026-09-02. Each row says whether it is SEEN — in the transcript or a screenshot — or BY THE LOOP: the authoring skill requires the move, and the screenshots do not show it. Nothing here is inferred from what the platform would do.
| # | What the agent did | Evidence |
|---|---|---|
| 1 | Loaded the a9script authoring skill. | seen |
| 2 | Listed the environment’s entities: tenant Demo, sandbox; two connections (jira, linear), two configured calls (JiraGetIssue, LinearGetIssues), one endpoint (jira). | seen |
| 3 | Read the jira endpoint’s own description. | seen |
| 4 | Probed both existing calls to see their real answer shapes (JiraGetIssue for KAN-8, LinearGetIssues). | seen |
| 5 | Read the authoring pack: the model of a script, then the function families it needed (core, mapping, state). | seen |
| 6 | Tried LinearGetIssues with a query argument and was refused: the function declares no parameter named query. It followed the refusal’s own next action. | seen |
| 7 | Created a discovery call, J2L: Linear Get Teams, as a file: apply —dry-run first, then apply, then probed it and read the team id off the answer. | seen |
| 8 | Wrote the integration as files: five configured calls, one settings script, four main scripts, one automation. | seen |
| 9 | Applied the folder. | by the loop |
| 10 | Ran six test runs with simulated events: an issue created, a comment, a status change, the hourly check, a comment carrying the prefix, an update without a status change. | seen |
| 11 | Checked the loop guards: the prefixed comment and the status-less update were skipped. | seen |
| 12 | Reported what it could not verify: live webhook delivery, one write probe the sandbox’s switch refused, the disconnect flow. | seen |
What it built
Configured calls (five): J2L: Linear Create Issue (GraphQL mutation) · J2L: Linear Create Comment (GraphQL mutation) · J2L: Linear Get Issue (GraphQL query, status by id) · J2L: Linear Get Teams (GraphQL query, used to discover the team id) · J2L: JIRA Add Comment (REST POST).
Settings (one): J2L Settings — the Linear team id, the mapping namespace
j2l-issues, the comment prefix [J2L].
Main scripts (four): J2L: On JIRA Issue Created (webhook, issue created — creates the Linear issue, stores the mapping, comments back on JIRA) · J2L: On JIRA Comment (webhook, comment created — forwards the comment to the mapped Linear issue) · J2L: On JIRA Status Change (webhook, issue updated — forwards a status change as a comment) · J2L: Poll Linear Status (schedule, every hour — checks every mapped Linear issue; completed or cancelled means a comment on the JIRA issue and the mapping deleted).
Automation (one): JIRA to Linear, grouping the four scripts.
Loop protection, as the agent stated it: every JIRA comment the automation
writes starts with [J2L]; the comment script skips a comment starting with
that prefix; the update script skips a webhook whose changelog carries no
status change.
The test runs
| Script | Input | Result |
|---|---|---|
| J2L: On JIRA Issue Created | simulated issue created for KAN-99 | created LEN-6 in Linear, mapping stored |
| J2L: On JIRA Comment | comment on KAN-99 | forwarded to LEN-6 |
| J2L: On JIRA Status Change | To Do → In Progress on KAN-99 | forwarded as a comment to LEN-6 |
| J2L: Poll Linear Status | empty | checked one mapping, LEN-6 still open, nothing closed |
| J2L: On JIRA Comment | a comment carrying the prefix | skipped, as the loop guard requires |
| J2L: On JIRA Status Change | a description change, no status | skipped |
Then a real ticket
A person created KAN-9 in JIRA by hand. The webhook reached the sandbox endpoint, the script ran, LEN-7 appeared in Linear with the JIRA key in its title, and JIRA received the comment [J2L] Linked to Linear: LEN-7 with the link to the Linear issue — written by the agent's own comment call, through the connection a person had entered.
The session's screenshots are the owner's and are added to this page as they are supplied, dated and masked.
What a person did
- Entered the JIRA connection, the Linear connection and the jira webhook endpoint in the browser, before the prompt — a credential is a person's to enter.
- Minted the agent's token for the sandbox without the credential facet, so it could neither create nor repoint a connection.
- Wrote the prompt, and read what came back.
- Created KAN-9 in JIRA by hand, watched LEN-7 appear in Linear, and read the linking comment on KAN-9.
Take-aways
- The agent never held a credential. It named the connections; the platform attached the credentials when the calls left.
- Only the logic is code: five calls, one settings script and four short main scripts. Retries, the record of every call and the id mapping were the platform's.
- A refusal taught it: an argument the function did not declare was refused by name, with the next move in the refusal, and the agent followed it.
- What it could not verify, it said — the block above is its own list, not ours.