Create a Rovo Action
Rovo actions are functions that a Rovo agent can call when answering a user. Each action maps to a named export in TypeScript and a declaration in manifest.yml. This guide walks through adding one end-to-end.
Where things live¶
forge/raise-jira/
manifest.yml ← action declarations + function mappings + agent configs
src/
agent/
actions.ts ← all action handler implementations
actions.test.ts ← action tests (Vitest)
remote/
platform-client.ts ← platformFetch() — authenticated client for raise-server
confluence/
skills.ts ← findSkillPage() — Confluence helper (used by find-skill)
Step 1 — Declare the action in manifest.yml¶
Open forge/raise-jira/manifest.yml. There are two places to edit.
1a. Add an action: entry¶
Under the action: section (after the last action, around line 310):
- key: my-new-action
name: My new action
function: fn-my-new-action
actionVerb: GET
description: One sentence describing what this action does and when Rovo should call it.
inputs:
requiredParam:
title: Required Param
type: string
required: true
description: What this param means (e.g., a Jira issue key like RAISE-1606)
optionalParam:
title: Optional Param
type: string
required: false
description: An optional filter or modifier.
key uses kebab-case. function is fn- + the same key. actionVerb is always GET for read operations.
1b. Add a function: entry¶
Under the # Agent actions comment in the function: section (around line 386):
handler format is agent/actions.<exportedFunctionName> — the part after the dot must exactly match the export name in actions.ts.
Step 2 — Implement the handler in actions.ts¶
Open forge/raise-jira/src/agent/actions.ts. Add a named export at the bottom.
Signature¶
export const myNewAction = async (payload: Record<string, unknown>): Promise<unknown> => {
// 1. Extract and validate inputs
// 2. Call backend (platformFetch or asApp().requestJira)
// 3. Return a plain object
};
All action handlers share this exact signature. Do not use the Resolver class — that pattern is for UI modules only. Rovo requires direct named exports.
Response conventions¶
Return a plain object. Use these shapes consistently:
| Situation | Shape |
|---|---|
| Success with data | { found: true, ...fields } |
| Not found (404) | { found: false, message: 'Human-readable explanation.' } |
| Input validation error | { error: 'Tell the user what is missing.' } |
| Backend error | { error: \Failed to ...: \${result.status}` }` |
Never throw. Return { error: ... } instead — Rovo passes these back to the agent's context.
Calling raise-server (platformFetch)¶
Use platformFetch for calls to the raise-server backend. It handles RSK auth automatically.
import { platformFetch } from '../remote/platform-client';
export const myNewAction = async (payload: Record<string, unknown>): Promise<unknown> => {
const { issueKey } = payload as { issueKey: string };
if (!issueKey) {
return { error: 'Please provide an issue key (e.g., RAISE-1606).' };
}
interface MyResponse {
field_one: string;
field_two: number;
}
const result = await platformFetch<MyResponse>(`/api/v2/my-endpoint/${encodeURIComponent(issueKey)}`);
if (!result.ok) {
if (result.status === 404) {
return { found: false, message: `No data found for ${issueKey}.` };
}
return { error: `Failed to fetch: ${result.status}` };
}
return {
found: true,
fieldOne: result.data.field_one,
fieldTwo: result.data.field_two,
};
};
platformFetch returns { ok: boolean; status: number; data: T }. On 204 (no body), data is {}.
Calling Jira directly (asApp().requestJira)¶
For Jira API calls (issue search, transitions, agile boards), import from @forge/api:
export const myNewAction = async (payload: Record<string, unknown>): Promise<unknown> => {
const { projectKey } = payload as { projectKey: string };
if (!projectKey) {
return { error: 'Please provide a project key (e.g., RAISE).' };
}
const { route, asApp } = await import('@forge/api');
const response = await asApp().requestJira(
route`/rest/api/3/search/jql`,
{
method: 'POST',
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
body: JSON.stringify({ jql: `project = "${projectKey}"`, maxResults: 10, fields: ['key', 'summary'] }),
},
);
if (!response.ok) {
return { error: `Jira search failed: ${response.status}` };
}
const data = (await response.json()) as { issues: Array<{ key: string }> };
return { issues: data.issues.map((i) => i.key) };
};
Use route\...`` (tagged template) for all Jira paths — it handles URL encoding and is required by Forge's permission model.
Step 3 — Register the action with an agent¶
Back in manifest.yml, find the agent that should offer this action. There are two:
raise-coach(lines ~98–167) — delivery health advisor; uses assessment and coaching actionsrai-for-rovo(lines ~169–231) — governance and project management; uses memory, graph, backlog actions
Under the agent's actions: list, add your action key:
- key: rai-for-rovo
...
actions:
- find-skill
- query-memory
- query-graph
- manage-backlog
- save-governance
- my-new-action # ← add here
An action can appear in multiple agents.
Step 4 — Write a test¶
Open forge/raise-jira/src/agent/actions.test.ts. Follow the existing pattern — one describe block per action, with beforeEach mocks.
import { myNewAction } from './actions';
// At the top of the file, alongside other vi.mock calls:
vi.mock('../remote/platform-client', () => ({
platformFetch: vi.fn(),
}));
// In the test file:
describe('myNewAction', () => {
let mockPlatformFetch: ReturnType<typeof vi.fn>;
beforeEach(() => {
vi.clearAllMocks();
const { platformFetch } = vi.mocked(await import('../remote/platform-client'));
mockPlatformFetch = platformFetch as ReturnType<typeof vi.fn>;
});
it('returns data for a valid input', async () => {
mockPlatformFetch.mockResolvedValue({
ok: true,
status: 200,
data: { field_one: 'hello', field_two: 42 },
});
const result = (await myNewAction({ issueKey: 'RAISE-1' })) as Record<string, unknown>;
expect(result.found).toBe(true);
expect(result.fieldOne).toBe('hello');
});
it('returns not-found for 404', async () => {
mockPlatformFetch.mockResolvedValue({ ok: false, status: 404, data: {} });
const result = (await myNewAction({ issueKey: 'RAISE-99' })) as Record<string, unknown>;
expect(result.found).toBe(false);
expect(typeof result.message).toBe('string');
});
it('returns error when input is missing', async () => {
const result = (await myNewAction({})) as Record<string, unknown>;
expect(typeof result.error).toBe('string');
});
});
Run tests:
Step 5 — Deploy¶
forge deploy uploads the new function binding. After deploy, the action is immediately available to the registered agents in Rovo.
Custom UI — first-time install in static/raise-app¶
If you also touch the Custom UI and need to rebuild it, pnpm install inside static/raise-app will fail with:
[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: esbuild, style-dictionary, ...
Run "pnpm approve-builds" to pick which dependencies should be allowed to run scripts.
Run this once to unblock it:
After that, go back to forge/raise-jira and run forge deploy as usual. Subsequent pnpm install runs in that directory will work without the approve step.
Summary of the three-point checklist¶
manifest.yml— oneaction:entry + onefunction:entry + agent referencesrc/agent/actions.ts— one named export matching the handler pathsrc/agent/actions.test.ts— success, 404, and validation-error cases
Common mistakes¶
Using Resolver instead of a named export. The Resolver class only works for UI module resolvers (issue panels, project pages). Rovo agent actions need direct export const name = ... — resolver.define(...) is ignored by the Forge agent runtime.
handler name mismatch. The part after the dot in handler: agent/actions.myFn must exactly match the TypeScript export name. A typo here causes a silent failure at runtime — the action appears registered but never executes.
Forgetting encodeURIComponent. Any user-supplied value interpolated into a platform URL path must be encoded. Jira keys like RAISE-1 are safe, but never assume.
Returning a thrown error instead of { error: ... }. Uncaught exceptions in Forge actions surface as opaque failures in Rovo. Always catch and return structured errors.