Run your first task
This guide builds one complete read operation. The name
menu.item.get remains unchanged from the tool definition through
the plan and handler.
This example uses a handwritten plan, so it does not require a model or API key.
pnpm add gruntend-sdk valibot
This guide uses Valibot, but Gruntend is not tied to one schema library. You can use Zod or any other validator that implements Standard Schema for tool inputs and outputs.
Define the capability
Section titled “Define the capability”Create tools.ts. defineTools() is contract-only. It describes the operation, validates its input and output, and produces its registered name. It does not access application data.
import { defineTools } from "gruntend-sdk/tool";
import * as v from "valibot";
export const tools = defineTools({
menu: {
item: {
get: {
description: "Get one menu item by id.",
input: v.object({
itemId: v.string(),
}),
output: v.object({
item: v.object({
itemId: v.string(),
name: v.string(),
price: v.number(),
}),
}),
},
},
},
});
The nested path creates exactly one registered tool:
menu.item.get
Implement the app-owned handler
Section titled “Implement the app-owned handler”Create handlers.ts. A handler is an async runtime closure owned by the application. It may access repositories, authorization state, secrets, and services that the generated plan cannot access.
For this first task, keep a small in-memory application repository in the same file:
const menuRepository = {
async get(itemId: string) {
if (itemId !== "item_1") return undefined;
return {
itemId: "item_1",
name: "Truffle Fries",
price: 8,
};
},
};
Now implement the handler:
import type { ToolHandlerMap } from "gruntend-sdk/tool";
import { tools } from "./tools.ts";
export const handlers = {
"menu.item.get": async ({ input, ok, err }) => {
const item = await menuRepository.get(input.itemId);
if (!item) {
return err({
code: "ITEM_NOT_FOUND",
message: "The menu item does not exist.",
retryable: false,
});
}
return ok({ item });
},
} satisfies ToolHandlerMap<typeof tools>;
Why the handler must return ok or err
Section titled “Why the handler must return ok or err”ToolHandlerMap<typeof tools> derives every handler type from the tool contracts. In this example, TypeScript checks that:
- the key is exactly
"menu.item.get" inputhas the type produced by the input schemaok(...)accepts only data matching the output schema- the handler returns either
ok(...)orerr(...)
Returning the output object directly is a type error:
return { item }; // TypeScript error
return ok({ item }); // valid handler result
ok and err are supplied in the handler context, so they are not imported.
return ok(output)completes the tool call. Gruntend validates and unwrapsoutput, so the plan receives the plain{ item }value.return err(error)reports an expected failure with a stablecodeandmessage. The optionalretryableflag controls whether Gruntend may try the call again.- Throw only for an unexpected fault.
TypeScript checks the application code while runtime schemas protect the boundary from generated plan values.
Write the plan
Section titled “Write the plan”Create run.ts. A plan calls the tool through the namespace implied by its registered name.
import { createGruntendClient } from "gruntend-sdk/client";
import { createJailJsCodePlanExecutor } from "gruntend-sdk/executor/jailjs";
import { handlers } from "./handlers.ts";
import { tools } from "./tools.ts";
const plan = `
const result = await tools.menu.item.get({
itemId: input.itemId,
});
return {
name: result.item.name,
price: result.item.price,
};
`;
The runtime unwraps the successful handler result before returning it to the plan. The plan receives { item }, not the internal Result wrapper.
Run the plan
Section titled “Run the plan”const gruntend = createGruntendClient({
tools,
executor: createJailJsCodePlanExecutor(),
});
const result = await gruntend.runCodePlan(plan, {
input: { itemId: "item_1" },
handlers,
});
For an existing item, result has this exact shape:
{
status: "done",
result: {
name: "Truffle Fries",
price: 8,
},
errors: {},
}
Understand the validation boundary
Section titled “Understand the validation boundary”The runtime validates both sides of the handler:
- The plan passes
{ itemId }. - The runtime validates that value with the tool input schema.
- Only valid input reaches
handlers["menu.item.get"]. - The handler returns
ok({ item }). - The runtime validates
{ item }with the output schema. - Only valid output returns to the plan.
Next: Code plans, Generated UI, or the API reference.