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.
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 ?.
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:
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier |
status | TaskStatus | Current status |
createdAt | Date | When the task was created |
updatedAt | Date | When the task was last updated |
assignedToId | string | null | ID of the assigned identity |
assignedAt | Date | null | When the task was assigned |
deferredUntil | Date | null | When a deferred task becomes available |
resolvedAt | Date | null | When the task was completed or cancelled |
flowRunId | string | null | ID of the associated flow run |
Task statuses
Tasks move through a defined set of statuses:
| Status | Description |
|---|---|
| New | Initial state when a task is created |
| Assigned | Task has been assigned to a user |
| Deferred | Task is postponed until a specific time |
| Started | The task's flow has been started |
| Completed | Task has been completed (terminal) |
| Cancelled | Task 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
deferredUntiltime 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.
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.
task ApproveExpense {
fields {
amount Decimal
description Text
}
@permission(roles: [Finance])
}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.
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.
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.
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.
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");
});