Sub-skills
Tasks in depth
For your agentCopy this and give it to the AI you're building with.
Run `cubek skill`, then `cubek skill tasks`, read all of both and follow them exactly (no CLI: download https://cubek.dev/SKILL.md and https://cubek.dev/skills/tasks.md verbatim with curl; don't summarize them). Run `cubek check` after every edit until it is clean.
Background work: queued tasks, delays, retries, cron, and running them by
hand. One concept: task.on registers a handler, task.run queues it,
task.schedule runs it on a cron.
// schema.ts
import { table, t } from "cubek"
export const orders = table("orders", {
id: t.id(),
email: t.string(),
status: t.enum(["held", "paid", "expired"]).default("held"),
receiptSentAt: t.timestamp().nullable(),
createdAt: t.timestamp().default("now"),
}).index("status", "createdAt")// src/tasks.ts
import { db, log, mail, task } from "cubek"
// Payload: plain data. A task may run more than once (a retry, a node
// failover): make it idempotent — check, act, record.
task.on("send-receipt", async (p: { orderId: string }) => {
const order = await db.orders.get(p.orderId)
if (order === null || order.receiptSentAt !== null) return
await mail.send({ to: order.email, subject: "Your receipt", text: `Order ${order.id} is paid.` })
await db.orders.update(order.id, { receiptSentAt: new Date().toISOString() })
})
// A hold that expires: queued with a delay when the hold is created.
task.on("expire-hold", async (p: { orderId: string }) => {
await db.orders.updateWhere({ id: p.orderId, status: "held" }, { status: "expired" })
})
// Cron (UTC, five fields): every day at 03:00.
task.on("cleanup", async () => {
const cutoff = new Date(Date.now() - 30 * 86400000).toISOString()
const n = await db.orders.deleteWhere({ status: "expired", createdAt: { lt: cutoff } })
log.info("cleanup", { deleted: n })
})
task.schedule("cleanup", "0 3 * * *")// src/app.tsx
import { app, db, task } from "cubek"
import "./tasks"
app.post("/orders", async (c) => {
const order = await db.orders.insert({ email: "buyer@example.com" })
await task.run("expire-hold", { orderId: order.id }, { delay: "15m" })
return c.json(order, 201)
})
app.post("/orders/:id/pay", async (c) => {
const id = c.param("id")
// The status change and the queued task commit together.
const ok = await db.transaction(async (tx) => {
const n = await tx.orders.updateWhere({ id, status: "held" }, { status: "paid" })
if (n === 1) await task.run("send-receipt", { orderId: id }, { retries: 5 })
return n === 1
})
return ok ? c.json({ ok }) : c.json({ error: "not held" }, 409)
})task.run(name, payload, { delay: "10m", retries: 3 })returns the task id at once; the request does not wait. Retries back off; a task that fails every attempt goes to the dead letters (cubek task list).- Inside
db.transaction,task.runis atomic with the writes. - Budgets per run: 5 min wall time, 500M instructions, 250 ms per query.
Split big jobs: process a batch of 500, then
task.runthe next batch with the last id. - Handlers register at module top level in a file the entry imports.
- Tasks run away from the request path: a slow
fetch(LLM, webhook delivery, mail API) belongs here, with the result written to the db (a live query shows it when it lands). - Cron: standard five fields, UTC (
"*/5 * * * *","0 9 * * 1-5"); each scheduled time queues one run for the app.
Running tasks by hand
cubek task list # dev: handlers, cron with next run, queue, dead letters
cubek task run cleanup # dev: run now, wait for it
cubek task run send-receipt '{"orderId":"01J..."}'
cubek task run cleanup --project shop # production revision (developer role)
cubek task list --project shop
cubek logs shop --level warn # what the runs loggedcubek dev runs cron and the queue locally; mail.send writes
.cubek/mail/*.eml there. mail.send({ to, subject, text, html?, attachments?: [{ filename, contentType, content }] }) throws when the
platform has no email configured.