Functions & access

Define a function once, call it from your app or an agent, and decide who may run it.

Runtime data4 min read

needs Postgres

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

graft.config.ts
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.

graft.config.ts
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:

app/contact/actions.ts
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

SettingEffect
A mutation with no access ruleSigned-in callers only. Anonymous calls are refused.
public: true and no access ruleAnyone, including anonymous visitors. Use it for public forms. When access is set, public is ignored and the rule decides.
A query with no access ruleAnyone. 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:

FieldWhat it is
ctx.inputThe request body, already checked against input and typed
ctx.actorWho is calling: a user, an agent, or anonymous
ctx.branchThe content branch the call runs on, usually main
ctx.dbThe database handle, for the record helpers
ctx.requestThe original HTTP request
ctx.correlationIdThe 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:

  1. The first call does not run. It files an approval request and answers 403 with the request's id.
  2. A person approves it, with graft approve <id> or in Studio.
  3. The caller repeats the call with the id, in the x-graft-approval header (HTTP) or the approval argument (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