Jobs allow you to execute tasks in the background, either manually or on a schedule as with cron (opens in a new tab). Jobs are Keel's answer to the collection of random scripts that every team ends up with and that one cron job which everyone forgets about but actually keeps the business running.

Jobs are written as normal TypeScript functions and have full access to your database. You can view job runs from the Keel console along with full traces and logs for easy debugging. You can limit who can run each job with role-based permission rules. Jobs can also accept inputs, making it possible to build simple but powerful internal tools.

Getting Started

Jobs are defined in your schema, with the implementation written in a TypeScript function.

A simple scheduled job
job EmailNewCustomerReport {
    @schedule("every weekday at 9am")

This example defines a scheduled job called EmailNewCustomerReport that will run every weekday (Monday-Friday) at 9am.

To scaffold out the code for this job you can run keel generate, which will create a new file in the ./jobs directory called emailNewCustomerReport.ts. This file will look something like this:

import { EmailNewCustomerReport } from "@teamkeel/sdk";
export default EmailNewCustomerReport(async (ctx) => {
  // your code here

Your job code can do whatever you want - call 3rd party APIs or use packages from npm. You can also interact with the database in the same way you would in functions, using the APIs available in the @teamkeel/sdk package.


The following limitations currently apply to jobs:

  • Jobs have 512MB of disk space available to them
  • A job can only run for 15 minutes, after this period it will be killed and marked as failed

Scheduled jobs

A job can be scheduled by using the @schedule attribute, which accepts a string that defines the schedule. The schedule string can either be a simple human-readable expression or for more complex cases a unix-style cron expression.

Defining a schedule

The human-readable form starts with the word every and then either describes a time interval (e.g. every 10 minutes), or a day and time (e.g. every monday at 9am). The interval form also allows for a time-range, for example every 2 hours from 8am to 6pm. Although the syntax of these expressions is very simple they support a lot of common scheduling patterns and have the benefit of being much easier to read than cron.

A scheduled job using human-readable expression
job EmailNewCustomerReport {
  @schedule("every monday at 9am")

These are the formats the human-readable schedule format accepts:

  • every N minutes
  • every N hours
  • every N minutes from T to T
  • every N hours from T to T
  • every D at T

Where N is an integer, T is a 12-hour time, and D is a day of the week. D can also be day for everyday or weekday for Monday-Friday.

In the format every D at T both D and T can be a list of values, for example every monday and tuesday at 9am, 12pm, and 3pm. The values must be comma-separated but the word and is also allowed to improve readability.

The cron form uses unix-style crontab, with five fields corresponding to minute, hour, day-of-month, month, and day-of-week. An example being 0 9 * * 1, which would trigger every Monday at 9am. Cron allows for very complex scheduling, for example */5 6-8,15-17 * 1 1,3,5, which would trigger every five minutes between 6am and 8am on Mondays, Wednesdays, and Fridays in January.

A scheduled job using cron syntax
job EmailNewCustomerReport {
  @schedule("0 9 * * 1")

The table below shows examples of the human-readable form with the equivalent cron syntax.

"every 10 minutes""*/10 * * * *"
"every 30 minutes from 10am to 2pm""*/30 10-14 * * *"
"every 2 hours""0 */2 * * *"
"every 2 hours from 9am to 3pm""0 9,11,13,15 * * *"
"every monday at 9am""0 9 * * 1"
"every tuesday and thursday at 8am, 12pm, and 5pm""0 8,12,17 * * 2,4"

If you're not familiar with cron syntax you can use a tool like (opens in a new tab) to help you build your schedule.


Keel will check your job's schedule at build time to make sure it is valid. Examples of validation errors are:

  • Having more than one @schedule attribute per job definition
  • A schedule that doesn't divide evenly into the time-unit e.g. every 5 hours would be an error because 24 does not divide evently by 5
  • Invalid cron syntax e.g. 0 9am * * * would result in the error invalid value '9am' for the hours field

Manual jobs

To be able to manually run a job from the console you must define a permission rule that specifies which members of your team are allowed to run the job. The following schema defines a DoImportantThing job which anyone with access to your Keel project can run.

A job that can be run by anyone with access to your project
job DoImportantThing {
  @permission(expression: true)

It's valid to add a @permission attribute to a job that also uses @schedule, which means the job will both run on the schedule and can be manually run from the console. Manual runs will not affect the scheduled runs in any way.


The previous example shows how to create a job that anyone with access to your project can run, but what if you only want to allow certain people in your team to be able to run a job? This is where role-based permissions are really useful.

The BigDataImport job defined in the following schema can only be run by someone with the Developer role, which is assigned to two members of the team - Sally and Barry.

A job with a role-based permission rule
job BigDataImport {
  @permission(roles: [Developer])
role Developer {
  emails {

The expression true when used in a job permission rule means that anyone with access to your project can trigger the job. Jobs are never publicly accessible.


Manual jobs can accept inputs which are provided when the job is run, allowing you to easily create internal tools for your whole team from simple functions. Any inputs defined in a job will be displayed as a form that is shown when trying to run the job in the console.

The following schema defines a RefundOrder job that accepts the order ID as an input.

A job that accepts inputs
job RefundOrder {
  inputs {
    orderId ID
  @permission(expression: true)

Whilst it is fine to have a scheduled job that also defines permissions for manual running, it is not valid to specify inputs for a job that is scheduled.

Jobs in the Console

From the Jobs page in the console you can see all the jobs defined in your project, when they last ran, and if applicable their schedule.

All jobs

The Run history tab shows all previous job runs. For each job you can see when it ran, if it ran successfully, and if manually executed who ran it. Clicking on an individual run row will show you the full trace and logs for that run, which can be useful for debugging failed jobs.

Job run history


To help you develop your jobs, Keel makes them available within tests. When calling a job from your test make sure to await it.

import { jobs, actions, resetDatabase } from "@teamkeel/testing";
import { test, expect, beforeEach } from "vitest";
test("EmailNewCustomerReport updates new customer flag", async () => {
  await jobs.emailNewCustomerReport();
  const res = await actions.getCustomer();