Publish your actions as a typed API
Turn existing actions into an OpenAPI document and a typed client, with an idempotent write and a breaking-change check in CI.
Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.
Stability The promise an interface ships with.
- Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
- Beta Usable in production; may change before it becomes stable.
- Experimental May change or go away; needs an explicit opt-in in main.yaml.
Your app already has actions. This guide publishes them over HTTP as an OpenAPI 3.1 document and a typed client, so a partner can call them without reading your code. It takes about 20 minutes.
Spec app-api, for 0.3. Run npx mainsdk api --help for the commands your version ships.
You need
- A MainSDK app with at least one action (see Expose an action to agents).
- An
outputschema on each action you want to publish. Without one, the document types the output as unknown.
1. Turn the API on
api:
base: /api
openapi:
serve: false
include: ["orders.*"] # only these actions are published
title: Acme API
snapshot: api/openapi.json
idempotency:
require: [destructive]include is a list of action name globs. Actions outside it stay private.
2. Mark the write that must not repeat
import { action } from "mainsdk";
import { s } from "mainsdk/schema";
export const refundOrder = action({
name: "orders.refund",
intent: "Refund an order the signed-in user owns",
input: s.object({ orderId: s.string().uuid(), reason: s.enum(["damaged", "late"]) }),
output: s.object({ refundId: s.string(), status: s.enum(["pending", "done"]) }),
risk: "destructive",
http: { method: "POST", path: "/orders/{orderId}/refund" },
run: async ({ input }) => ({ refundId: `r_${input.orderId}`, status: "pending" as const }),
});A destructive action now needs an Idempotency-Key header. A repeat with the same key returns the first answer.
3. Write the document and the client
npx mainsdk api openapi --out api/openapi.json
npx mainsdk api client --out src/acme-api.tsCommit both files. api/openapi.json is the snapshot that mainsdk check compares against.
4. Call it from a script
import { createClient } from "./acme-api.ts";
const acme = createClient({ baseUrl: "https://acme.app", auth: { apiKey: process.env.ACME_KEY ?? "" } });
const orderId = "2c8b1c1e-7d7e-4b8e-9d0a-5d1a3a8f4a11";
await acme.orders.refund({ orderId, reason: "late" }, { idempotencyKey: "refund-8812" });5. Stop breaking changes in CI
npx mainsdk api diff --base origin/mainAdd it to the pull request job. A breaking change in a major version that has shipped fails with MAIN-API-0012. To change it, add version: 2 to the action and keep version 1 until its sunset date.
Check it worked
mainsdk checkpasses, andapi/openapi.jsonmatches the code.- Calling the refund twice with the same
Idempotency-Keyreturns the same answer and writes once. - Calling it without the key returns 400 with
MAIN-API-0005. - Removing a field from
outputmakesapi difffail.
If it fails
MAIN-API-0011: the snapshot is stale. Runapi openapiagain and commit the result.MAIN-API-0010: a schema has no JSON Schema. Use a schema frommainsdk/schema.
Prompt for your coding agent
Publish the actions under
orders.*as an OpenAPI document and a typed client using MainSDK'sapiinterface. Makeorders.refunda destructive write that requiresIdempotency-Key, keepopenapi.serveoff, and addmainsdk api diffto the pull request job.
Was this page helpful?