8 September, 2026
This release lets your functions run the actions declared in your schema, through a new actions export on the generated SDK, so backend code goes through the same validation and permission rules as any other caller.
Calling actions from functions
@teamkeel/sdk now exports an actions object with a typed method for every action your APIs expose. Where models.item.create() writes the row behind the Item model, actions.createItem() runs the createItem action the way an API caller would, so its input validation, @permission rules, @validate rules, action hooks and events all apply:
import { ImportItem, actions } from "@teamkeel/sdk";
export default ImportItem(async (ctx, inputs) => {
const item = await actions.createItem({
sku: inputs.sku,
name: inputs.name,
});
return { id: item.id, sku: item.sku, name: item.name };
});The actions object is available anywhere the SDK is: custom functions, action hooks, jobs, subscribers, flow function steps and route handlers. The methods are typed from your schema, so the inputs and the response are the ones the action declares, and only the actions an api block exposes get a method at all.
The part to think about before you use it is who the call runs as. A bare call carries an access token for the identity your code is already running as: a request-triggered function forwards the token its caller presented, and a flow step or a manually run job gets a short-lived token for the person who started it. A scheduled job, a subscriber and an unauthenticated request carry no token, so their calls run anonymously and only reach actions whose rules allow that.
That means the action's rules apply to your function's work exactly as they would to a direct API call. There is no backend bypass and no identity override, so a function can do no more than its caller could have done. Where you need a call to run as somebody else, hold a token for them and pass it to withAuthToken:
const item = await actions.withAuthToken(token).createItem(inputs);A failed call throws an ActionError carrying the code and status the JSON API would have returned, which isActionError narrows:
import { actions, isActionError } from "@teamkeel/sdk";
try {
await actions.renameItem({ where: { id }, values: { name } });
} catch (e) {
if (isActionError(e, "ERR_RECORD_NOT_FOUND")) {
return null;
}
throw e;
}An ActionError you leave uncaught is not flattened into a generic internal error. It reaches the original caller as the same kind of error the inner action reported, so a permission failure inside your function surfaces as ERR_PERMISSION_DENIED and a missing record as ERR_RECORD_NOT_FOUND.
One behaviour to plan for: the runtime executes an action call on its own database connection, in its own transaction, so it never joins the transaction your function is running in. A row your function has written but not yet committed is invisible to the action, and a foreign key pointing at it fails. For a write function or a write hook that calls a mutating action, set dbTransaction: false so each step commits as it runs. Read-only functions have no transaction to begin with and need no change.
Finally, a function can call an action that is itself a function, which can call further actions. The runtime refuses a chain deeper than 10 nested calls with ERR_INVALID_INPUT, which is what stops a function that calls itself from recursing forever.
See the Actions API for the full details, including the complete list of error codes, how file inputs are handled, and withTimezone.
For any issues or feedback, please contact us at help@keel.so.
Thank you for using Keel!