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.run is 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.run the 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 logged

cubek 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.