Tasks

Tasks let you define work queues that can be assigned to users, tracked through a lifecycle, and processed via flows. Each task you define in the schema gets a database-backed queue with built-in status tracking, assignment, deferral, and an audit history of all state transitions.

Defining a task

Tasks are declared in your schema using the task keyword. Each task has custom fields, permissions, and an optional ordering.

schema.keel
task FulfillOrder {
    fields {
        orderDate Date
        shipByDate Date
    }
 
    @permission(roles: [Warehouse])
    @orderBy(shipByDate: asc, orderDate: asc)
}
 
role Warehouse {
    domains {
        "acme.com"
    }
}

Run keel generate to scaffold the task flow implementation file at tasks/fulfillOrder.ts.

Fields

Define your task's data in the fields block. Supported types are Text, Number, Boolean, Date, Timestamp, Decimal, and any enum defined in your schema. Fields can be made optional with ?.

schema.keel
enum Priority {
    Low
    Medium
    High
}
 
task ReviewOrder {
    fields {
        customerName Text
        orderTotal Decimal
        priority Priority
        notes Text?
    }
 
    @permission(expression: true)
}

Every task also includes these built-in fields automatically:

FieldTypeDescription
idstringUnique identifier
statusTaskStatusCurrent status
createdAtDateWhen the task was created
updatedAtDateWhen the task was last updated
assignedToIdstring | nullID of the assigned identity
assignedAtDate | nullWhen the task was assigned
deferredUntilDate | nullWhen a deferred task becomes available
resolvedAtDate | nullWhen the task was completed or cancelled
flowRunIdstring | nullID of the associated flow run

Task statuses

Tasks move through a defined set of statuses:

StatusDescription
NewInitial state when a task is created
AssignedTask has been assigned to a user
DeferredTask is postponed until a specific time
StartedThe task's flow has been started
CompletedTask has been completed (terminal)
CancelledTask has been cancelled (terminal)

State transitions

New ──→ Assigned ──→ Started ──→ Completed
 │          │                        ▲
 │          │                        │
 │          ▼                (auto when flow
 │       Deferred             completes)
 │          │
 │          │ (when deferredUntil passes,
 │          │  becomes available again)
 │          ▼
 └──────→ Cancelled
  • New → Assigned: When a task is assigned to a user, either directly or via the next-task endpoint
  • Assigned → Started: When start() is called, which creates and runs the associated flow
  • Started → Completed: Automatic when the flow finishes
  • Any open status → Cancelled: A task can be cancelled at any time before it is resolved
  • New/Assigned → Deferred: The task is postponed. Deferred tasks are excluded from the queue until their deferredUntil time passes
  • Assigned → New: When a task is unassigned, it returns to the queue

Completed and Cancelled are terminal statuses. Once a task reaches either of these states, no further transitions are allowed.

Every status change is recorded in an audit history, tracking who performed the action and when.

Ordering

Use @orderBy to control the order in which tasks are returned from the queue. You can specify multiple fields with asc or desc directions.

schema.keel
task ProcessReturn {
    fields {
        priority Number
        requestedAt Timestamp
    }
 
    @permission(expression: true)
    @orderBy(priority: desc, requestedAt: asc)
}

This ensures high-priority tasks are processed first, and tasks with equal priority are processed in the order they were requested.

Permissions

Tasks use the @permission attribute to control access. All task operations require authentication. You can use role-based or expression-based permissions.

schema.keel
task ApproveExpense {
    fields {
        amount Decimal
        description Text
    }
 
    @permission(roles: [Finance])
}
schema.keel
task VerifyDocument {
    fields {
        documentId Text
    }
 
    @permission(expression: ctx.isAuthenticated)
}

Implementing a task flow

Each task has an associated flow with the same name. The flow is where you implement the business logic that runs when a task is started. Flow files live in the tasks/ directory.

tasks/fulfillOrder.ts
import { FulfillOrder } from "@teamkeel/sdk";
 
export default FulfillOrder({}, async (ctx, task) => {
    // task contains all your custom fields
    const order = await models.order.findOne({
        orderDate: task.orderDate,
    });
 
    await ctx.step("pack order", async () => {
        // Pack the order...
    });
 
    await ctx.step("ship order", async () => {
        // Ship the order...
    });
});

When the flow completes, the task is automatically marked as Completed. You can also use ctx.complete() to explicitly complete the flow with return data.

tasks/fulfillOrder.ts
import { FulfillOrder } from "@teamkeel/sdk";
 
export default FulfillOrder({}, async (ctx, task) => {
    const trackingNumber = await ctx.step("ship order", async () => {
        // Ship and return tracking number
        return "TRACK-12345";
    });
 
    return ctx.complete({
        data: {
            trackingNumber,
            shippedAt: new Date().toISOString(),
        },
    });
});

Task flows support all flow features including function steps, UI steps, and automatic retries.

Using the Tasks SDK

The Tasks SDK lets you create and manage tasks programmatically from functions, flows, and subscribers.

Import tasks from @teamkeel/sdk:

import { tasks } from "@teamkeel/sdk";

Creating tasks

const task = await tasks.fulfillOrder.withIdentity(ctx.identity).create({
    orderDate: new Date("2025-07-15"),
    shipByDate: new Date("2025-07-30"),
});
 
// task.id - unique task ID
// task.status - "New"

To create a deferred task that won't appear in the queue until a specific time:

const task = await tasks.fulfillOrder.withIdentity(ctx.identity).create(
    {
        orderDate: new Date("2025-07-15"),
        shipByDate: new Date("2025-07-30"),
    },
    {
        deferredUntil: new Date("2025-08-01"),
    },
);
 
// task.status - "Deferred"

Managing tasks

Task instances have methods for each state transition:

// Assign to a specific user
await task.assign({ identityId: userId });
 
// Start the task (creates and runs the flow)
await task.start();
 
// Defer until a later time
await task.defer({ deferUntil: new Date("2025-08-15") });
 
// Complete the task
await task.complete();
 
// Cancel the task
await task.cancel();
 
// Return to the queue (unassign)
await task.unassign();

Switching identity

Use withIdentity() or withAuthToken() to perform actions as a different user:

// On the tasks module
const task = await tasks.fulfillOrder.withIdentity(otherUser).create({...});
 
// On a task instance
await task.withIdentity(manager).assign({ identityId: manager.id });
await task.withAuthToken(token).complete();

Creating tasks from within flows

Flows can create new tasks using the SDK. This is useful for workflows that spawn sub-tasks.

tasks/processOrder.ts
import { ProcessOrder, tasks } from "@teamkeel/sdk";
 
export default ProcessOrder({}, async (ctx, task) => {
    // Create a child task for each line item
    for (const item of task.items) {
        await tasks.pickItem.withIdentity(ctx.identity).create({
            orderId: task.orderId,
            sku: item.sku,
            quantity: item.quantity,
        });
    }
});

Querying tasks

Use findOne and findMany to query tasks:

const task = await tasks.fulfillOrder.withIdentity(ctx.identity).findOne({
    id: taskId,
});
 
const openTasks = await tasks.fulfillOrder.withIdentity(ctx.identity).findMany({
    status: "New",
});

Testing

Tasks are available in tests through the @teamkeel/sdk import.

tasks/fulfillOrder.test.ts
import { resetDatabase, models, flows } from "@teamkeel/testing";
import { tasks } from "@teamkeel/sdk";
import { test, expect, beforeEach } from "vitest";
 
beforeEach(resetDatabase);
 
test("fulfill order - creates and completes task", async () => {
    const token = await getToken({ email: "warehouse@acme.com" });
 
    // Create a task
    const task = await tasks.fulfillOrder.withAuthToken(token).create({
        orderDate: new Date("2025-07-15"),
        shipByDate: new Date("2025-07-30"),
    });
    expect(task.status).toBe("New");
 
    // Assign the task
    const identity = await models.identity.findOne({
        email: "warehouse@acme.com",
        issuer: "https://keel.so",
    });
    await task.assign({ identityId: identity.id });
    expect(task.status).toBe("Assigned");
 
    // Start the task (runs the flow)
    await task.start();
    expect(task.status).toBe("Started");
    expect(task.flowRunId).toBeDefined();
 
    // Wait for the flow to finish
    const completed = await flows.fulfillOrder
        .withAuthToken(token)
        .untilFinished(task.flowRunId);
    expect(completed.status).toBe("COMPLETED");
});