Functions & access
Define a function once, call it from your app or an agent, and decide who may run it.
A typed function is a piece of server code you declare in your config, such as "save a contact-form submission" or "list this week's orders". It is the only way to read or write operational data, and it is how agents run your business logic too.
You write the handler. Graft does the rest around it: it checks the input
against a schema, decides whether the caller is allowed in, applies rate
limits, records the call in the audit log, and returns errors with a fix.
This page shows how to declare a function, call it, and control access.
Declare a function
export const submitContact = defineFunction({
name: "submitContact",
kind: "mutation", // "mutation" changes data, "query" only reads it
public: true, // allow visitors who are not signed in
rateLimit: { limit: 5, windowSeconds: 60 },
input: submissions.fields,
handler: async (ctx) => {
const record = await insertRecord(ctx, submissions, ctx.input);
return { id: record.id, receivedAt: record.createdAt.toISOString() };
},
});Then add it to the functions record the same file exports. Graft serves only
the functions in that record. Exporting the function on its own is not enough:
a call to it answers 404 FUNCTION_NOT_FOUND.
export const functions = { submitContact };It is now served at POST /api/fn/submitContact.
Call it from your app
Send the input as a JSON object:
const response = await fetch("/api/fn/submitContact", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ email, message }),
});
const result = await response.json();
// success: { data: { id, receivedAt } }
// failure: { error, message, fix }Every response, success or failure, carries an x-graft-correlation-id header.
Log it: it is how you find this exact call in the
audit log when a user reports a problem.
Agents call the same function through the run_function MCP tool, with the same
checks. To serve the endpoint, mount Graft in your framework (see your
framework guide) or
run graft serve.
Who can call it
| Setting | Effect |
|---|---|
A mutation with no access rule | Signed-in callers only. Anonymous calls are refused. |
public: true and no access rule | Anyone, including anonymous visitors. Use it for public forms. When access is set, public is ignored and the rule decides. |
A query with no access rule | Anyone. Add a rule to any query that returns private data. |
access: requireScopes("submissions:read") | Only callers whose token carries that scope. |
Graft works out who the caller is from their token. Auth covers how tokens are checked and where scopes come from.
What your handler receives
The handler gets one argument, ctx:
| Field | What it is |
|---|---|
ctx.input | The request body, already checked against input and typed |
ctx.actor | Who is calling: a user, an agent, or anonymous |
ctx.branch | The content branch the call runs on, usually main |
ctx.db | The database handle, for the record helpers |
ctx.request | The original HTTP request |
ctx.correlationId | The id returned in x-graft-correlation-id |
Read and write data with the record helpers (insertRecord, listRecords,
updateRecord, deleteRecord, searchRecords). Pass ctx as the first
argument. They check every record against its schema and stamp it with the
caller and correlation id. Operational data shows
each one.
Protect functions that destroy data
Mark a function destructive: true when its effect cannot be undone, such as
deleting records. Under the default approval policy, it then needs a human's
approval before it runs:
- The first call does not run. It files an approval request and answers
403with the request's id. - A person approves it, with
graft approve <id>or in Studio. - The caller repeats the call with the id, in the
x-graft-approvalheader (HTTP) or theapprovalargument (MCP). Now it runs.
An approval works once, and only for the exact input that was approved.
The approval policy in graft.config.ts changes who waits. "human" gates
every mutation, not only destructive ones. "unattended" applies to HTTP calls
only: there it gates nothing, so destructive functions run straight away. It
exists for scheduled jobs with no person behind them. Over MCP, destructive
calls still wait for a person. Every call is audited either way.
Audit & approvals covers the details, including
asking for approval from inside an agent's session.
Where functions can run
A function is a standard web handler: it takes a Request and returns a
Response, and keeps no state between calls. The same function runs inside any
of the framework adapters, in the Graft container, or on serverless platforms,
without changes.
Next steps
- Operational data shows the record helpers in a complete example.
- Auth sets up identity and scopes.
- Rate limits protects public functions.