Skip to main content
An operation declares one business change: which kind of object it is about, which arguments it takes, what local result it returns, and how it prepares the change. Preparation runs in an ordinary mutation transaction.
bijection/invoices.ts
Read on to understand each part of the declaration.

Operation names

Operations follow the same naming rules as queries, see Query names. An operation exported as addNote from bijection/invoices.ts is addressed as invoices:addNote. Operations can be defined in the same file as queries, mutations and actions.

The defineOperation constructor

Import defineOperation from bijection/server and pass it an object with these fields: defineOperation refuses a declaration that is missing on, args, returns or prepare.
Once your deployment declares an operation, the generated ./_generated/server also exports a defineOperation typed with your data model, just like mutation.

The object type: on

on associates the operation with a business object type. Usually this is a view registered in your schema:
bijection/schema.ts
on can also name a locally owned table declared with .retain(...). In that case the operation must declare a target. The association is used for discovery, for example to list the operations available on an invoice. It grants no permission by itself.

The target: target

target: { argument } names the argument that selects the one existing object the operation changes. That argument must be a required, top-level v.id(...) argument. For a view, the ID belongs to the view’s key table; for a retained table, to that table. For a retained table, the target row is read through the ordinary tracked reader before preparation runs, so the operation depends on it even if prepare does not read it. Leave target out for an operation that creates a new object, or that is not about one particular existing object.

Arguments and result

args and returns use the same validators as queries and mutations. Both are required. returns describes the operation’s local result: what preparation produced in this transaction. It says nothing about what an external system later does. Preparation must return a value that matches it.

Preparation: prepare

prepare receives a mutation context and the validated arguments. It runs as one transaction, exactly like a mutation:
  • Use ctx.db to read and write your tables.
  • Use ctx.externalCalls.submit to request work in an external system. Read on about External calls.
  • Use ctx.auth to check the caller, and ctx.scheduler to schedule functions.
Like any mutation, preparation cannot call third-party APIs itself. If it throws, nothing it wrote and no external call it submitted is kept.
bijection/orders.ts
To refuse a request with a reason the caller can show, throw a BijectionError whose data has a string code and message. A preview reports exactly those two fields, so a person sees why before they submit.

Internal operations

defineInternalOperation takes the same fields and declares an operation that can only be called from your own server code, like an internal function. Internal visibility does not grant permission to call it: the caller still needs a grant.
bijection/orders.ts

Granting access

Every caller of an operation, including a deployment administrator, needs an explicit grant on that operation. The permissions are read, preview and invoke. A deployment administrator grants them with bijection permissions:
The subject is a user token identifier, the issuer|subject pair. Read on about access rules and grants.

Operations after a deploy

An accepted request keeps the meaning it was accepted with. Deploying new code does not rerun preparation for requests that were already accepted, and does not recalculate their external calls. Each generated reference carries the operation’s contract. If the deployed operation’s contract (its object type, target, arguments or result) no longer matches the reference a caller was built with, the call is refused with OperationDefinitionChanged. Regenerate your code and call it again.