Build for Dunea
AI tools
Give the Dunea AI new abilities: define tools, write descriptions it can use, and answer within 15 seconds.
agentTools lets the Dunea AI call your service while it builds or manages an owner's app. When the plugin is installed, its tools join the AI's toolset for that app. The AI reads each tool's description and input to decide when to call it and with what, and reads your response to decide what to do next.
For example, a print-on-demand plugin could offer create_print_product. When the owner says "Sell my sunset artwork as a poster", the AI calls the tool with a title and artwork URL, and your service creates the product.
Define a tool
defineTool gives you the manifest entry and a typed handler from one definition, so the two cannot drift apart:
import { defineTool, DuneaClient } from '@dunea/plugin-sdk';
// Your own storage: the token you saved from plugin.installed.
declare function getAccessToken(installId: string): Promise<string>;
export const createPrintProduct = defineTool({
name: 'create_print_product',
description:
'Create a print-on-demand poster product in the shop from an artwork image URL. ' +
'Use when the owner wants to sell artwork as a printed product. Returns the new product id.',
input: {
type: 'object',
properties: {
title: { type: 'string', description: 'Product title shown to buyers, e.g. "Sunset poster"' },
artworkUrl: { type: 'string', description: 'https URL of the artwork image' },
size: { type: 'string', description: 'Poster size', enum: ['A3', 'A2', 'A1'] },
priceCents: { type: 'integer', description: 'Retail price in cents, e.g. 2900 for $29.00' },
},
required: ['title', 'artworkUrl'],
},
handler: async (input, request) => {
// input.title and input.artworkUrl are strings; size and priceCents are optional
const dunea = new DuneaClient({ accessToken: await getAccessToken(request.installId) });
const product = await dunea.products.create({
title: input.title,
price: input.priceCents ?? 2900,
status: 'draft',
images: [{ url: input.artworkUrl, alt: input.title }],
});
return {
output: `Created draft product "${product.title}" (${product.id}). It is not visible until the owner activates it.`,
data: { productId: product.id },
};
},
});Put the manifest entry in agentTools, and pass the tool to your webhook handler:
import { definePlugin, createWebhookHandler, toolManifests } from '@dunea/plugin-sdk';
import { createPrintProduct } from './tools';
export const manifest = definePlugin({
// ...
permissions: ['shop:write'],
webhookUrl: 'https://plugins.example.com/dunea',
agentTools: toolManifests([createPrintProduct]),
});
export const handler = createWebhookHandler({
clientSecret: process.env.DUNEA_CLIENT_SECRET!,
onToolCall: [createPrintProduct],
});If you keep the manifest as dunea.plugin.json (what the CLI reads), paste the output of JSON.stringify(createPrintProduct.manifest) into agentTools, or generate the JSON file from the TypeScript manifest as shown in the manifest reference.
The handler created from defineTool checks the input against your schema before your code runs (required fields present, types and enum values correct). If the check fails, the AI gets an error describing the problem and can try again.
The request
Tool calls arrive at your webhookUrl, signed the same way as events:
interface ToolCallRequest {
id: string;
type: 'tool_call';
tool: string; // your tool's name
input: Record<string, unknown>;
installId: string;
appId: string;
mode: 'test' | 'live';
}Use installId to look up the install's access token, and respect mode: in "test", do not take real-world actions such as charging a card or shipping a parcel.
The response
Answer with a 200 and this JSON:
interface ToolCallResponse {
/** Shown to the Dunea AI. Short and factual; markdown is fine, at most 8 KB. */
output: string;
/** Optional structured data the AI can use. */
data?: Record<string, unknown>;
/** Set to report a failure the AI should explain to the user. */
error?: string;
}A handler can also return a plain string, which becomes { output: string }.
outputis what the AI reads. Say what happened and what the owner should know, in a sentence or two. Keep it under 8 KB.datacarries IDs and values the AI may need for a next step, likeproductId.erroris for failures the owner should hear about: a missing setting, an expired account on your side, a product that does not exist. The AI explains it to the owner. A response witherroris never charged, even for a priced tool. WithcreateWebhookHandler, an exception thrown in your handler becomes{ "output": "", "error": "<message>" }, so make your error messages readable.
Timeouts
Respond within 15 seconds. The AI is waiting while your tool runs. If the work takes longer, start it, respond with what you started, and finish in the background:
return { output: 'Started generating 12 mockups. They will appear in the product gallery in a few minutes.' };Charging for a tool
A tool can have a price per successful call, in Dunea credits, paid by the owner within a monthly spend cap they set:
{ "name": "create_shipping_label", "description": "…", "input": { "…": "…" }, "price": { "credits": 2 } }The AI is told the price before it calls the tool. The owner is charged only when your service answers 2xx without error, and a retried call (the same id) is charged once. Dev installs and your own apps are never charged. See Priced tools and metered usage.
Writing good tool descriptions
The description is the only documentation the AI has. It decides whether to call your tool from it.
- Say what it does and when to use it. "Create a shipping label for a paid order. Use after an order is paid, when the owner asks to ship it." beats "Shipping tool."
- Name the inputs plainly, with units and formats: "Price in cents", "ISO country code, e.g. DE", "https URL of the image".
- Use
enumwhenever there is a fixed set of values. Calls with a value outside it are rejected before your handler runs. - Mark only truly needed fields as
required. Give optional fields sensible defaults in your handler. - Say what it returns, and any side effects the owner should know about ("Sends an email to the buyer").
- Say what it costs, if it has a price: "Costs 2 credits per label." Prices must also be in your listing's description (
DUN-B002). - One job per tool. Two clear tools are easier for the AI to use than one tool with a
modeswitch. - Keep names specific:
create_shipping_label, notrunordo_action. Names are lowercase with underscores.
Limits: up to 10 tools per plugin, descriptions 20 to 600 characters, property descriptions up to 300 characters, and inputs are flat (string, number, integer, boolean). See the manifest reference.
Tools act on the owner's app with the install's permissions. A tool that writes data needs the matching *:write permission, and the AI cannot do more through your tool than the owner approved.