Level 2 · Lesson 5
Hand dishes on at the pass
Level 2 · Lesson 5A second trigger watches for specialist dishes moved to Approved for Agent, checks four gates, and hands each one on with a comment, exactly once.
Behind, or starting here? Download the code as it should be before this lesson.
Some dishes need research before anyone cooks them, such as the tom yum soup whose notes ask for a lemongrass supplier. The sous-chef already labels those assign-to-agent. A person still decides when one is ready: they move it to Approved for Agent, and the app hands it on.
Someone moves a specialist work item to Approved for Agent.
In the kitchen: A specialist dish is cleared at the pass.
Atlassian’s own products · House systems
Jira work items Dish tickets
A Jira issue. In this course each work item names one dish, such as “Pad thai”.
A ticket on the board for one dish.
Select any box in the diagram to see what it is.
Step 1: Four gates, then hand it on once
Locked until the step before it is done.
Runs in the Atlassian cloud · Back of house
Agent handoff · the pass
The handoff is a trigger that runs on work item updates and acts only when every gate passes: the ticket is in the Kitchen space, it has the assign-to-agent label, its status is Approved for Agent, and it has not been handed on before. Then it comments once and records that it did, so moving the ticket back and forth never hands it on twice.
In the kitchen Dishes cleared at the pass go to the specialist, once each.
A new file holds the gates:
src/handoff.jsA new file: read the ticket, check four gates, comment, and remember.
Added: import api, { route } from '@forge/api'; |
Added: import { kvs } from '@forge/kvs'; |
Added: import { LABELS, STORE, isIssueKey, settings } from './lib/config'; |
Added: import { paragraphs } from './lib/adf'; |
Added: import { comment } from './kitchen/comment'; |
Added: |
Added: // Events carry only some fields, so read the ones the gates need from Jira itself. |
Added: async function readTicket(issueKey) { |
Added: const response = await api.asApp().requestJira(route`/rest/api/3/issue/${issueKey}?fields=labels,status,project`); |
Added: if (!response.ok) throw new Error(`Jira answered ${response.status} for ${issueKey}.`); |
Added: const issue = await response.json(); |
Added: return issue.fields; |
Added: } |
Added: |
Added: /** The pass: hands a cleared specialist dish on, once. Runs on updated and assigned events. */ |
Added: export async function run(event) { |
Added: const issueKey = event.issue?.key; |
Added: if (!isIssueKey(issueKey)) return; |
Added: const { projectKey, handoffStatus } = settings(); |
Added: const fields = await readTicket(issueKey); |
Added: if (fields.project?.key !== projectKey) return; // 1. a ticket on our board |
Added: if (!fields.labels?.includes(LABELS.SPECIALIST)) return; // 2. marked for the specialist |
Added: if (fields.status?.name !== handoffStatus) return; // 3. cleared at the pass |
Added: if (await kvs.get(`${STORE.HANDOFF}${issueKey}`)) return; // 4. not handed on before |
Added: await comment(issueKey, paragraphs('Agent task triggered. This dish is ready for the specialist chef.')); |
Added: await kvs.set(`${STORE.HANDOFF}${issueKey}`, { at: new Date().toISOString(), eventType: event.eventType }); |
Added: console.log(`Pass: handed ${issueKey} to the specialist.`); |
Added: } |
Why: Agent handoff · Key-Value Store (KVS) · asApp()
Jira’s update events carry only some of a work item’s fields, and labels are not among them. So the function reads the labels, status and space from Jira itself instead of trusting the event. The record in the jar is the fourth gate: it is written only after the comment succeeds.
Stuck? Moving a ticket to “Approved for Agent” does nothing
Next step: next
Step 2: A second bell, at the pass
Locked until the step before it is done.
The new trigger listens for updates and assignments, and, like the bell since lesson 2, ignores what the app did itself. Its function is the second change:
manifest.ymlChange 1 of 2Listen for updates and assignments, but not the app’s own.
- avi:jira:created:issue |
filter: |
ignoreSelf: true |
Added: - key: the-pass |
Added: function: handoff |
Added: events: |
Added: - avi:jira:updated:issue |
Added: - avi:jira:assigned:issue |
Added: filter: |
Added: ignoreSelf: true |
llm: |
- key: sous-chef |
model: |
manifest.ymlChange 2 of 2The function it runs.
- key: menu-worker |
handler: worker.run |
timeoutSeconds: 120 |
Added: - key: handoff |
Added: handler: handoff.run |
resources: |
- key: main |
path: src/frontend/index.jsx |
Why: Product trigger · ignoreSelf · Agent handoff
Without ignoreSelf, the app’s own comment and edits could wake the trigger again. The fourth gate would still stop a second comment, but every edit would cost a run.
Stuck? I changed manifest.yml · Moving a ticket to “Approved for Agent” does nothing
Next step: next
Step 3: Deploy
Locked until the step before it is done.
The app already has every scope this needs, so this is a minor version:
npm run build:ui
forge deployStuck? Deploy stops after a permission change · The tunnel is running while I deploy or upgrade
Next step: next
Step 4: Clear a dish at the pass
Locked until the step before it is done.
- Move the Tom yum soup ticket to Approved for Agent. Within a few seconds it gets a comment: “Agent task triggered. This dish is ready for the specialist chef.”
- Move it to another column and back again. No second comment.
- Move a dish without the
assign-to-agentlabel to Approved for Agent. Nothing happens.
forge logs shows one Pass: line for the hand-off.
Stuck? Moving a ticket to “Approved for Agent” does nothing
Next step: next
Compare with yours: download the code after this lesson.