Course menuForge Kitchen

Level 2 · Lesson 5

Hand dishes on at the pass

A second trigger watches for specialist dishes moved to Approved for Agent, checks four gates, and hands each one on with a comment, exactly once.

About 25 min4 steps

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.

The handoff trigger

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

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:

New filesrc/handoff.js

A 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

Step 2: A second bell, at the pass

The updated event reaches the handoff function. Events the app caused itself are filtered out.The second bell rings, but not for the kitchen’s own changes.

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:

Editmanifest.ymlChange 1 of 2

Listen 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:
Editmanifest.ymlChange 2 of 2

The 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

Step 3: Deploy

The app already has every scope this needs, so this is a minor version:

RunTerminal
npm run build:ui
forge deploy

Stuck? Deploy stops after a permission change · The tunnel is running while I deploy or upgrade

Step 4: Clear a dish at the pass

  1. 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.”
  2. Move it to another column and back again. No second comment.
  3. Move a dish without the assign-to-agent label 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

Compare with yours: download the code after this lesson.

Type at least two letters.