avoid-feature-creep
Keeps a change scoped to what was asked. Flags extra features, refactors, and 'while we are here' edits, and moves them to a…
Writes Convex queries, mutations, actions, and internal functions in the object form with args and returns validators, correct ctx usage, runtime boundaries, and error handling. Use when adding or changing anything in convex/*.ts that exports a function, or when deciding between
$ npx -y skills add waynesutton/builder-skills --skill convex-functions --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/convex-functionsContext preview
The summary Claude sees to decide when to auto-load this skill.
Writes Convex queries, mutations, actions, and internal functions in the object form with args and returns validators, correct ctx usage, runtime boundaries, and error handling. Use when adding or changing anything in convex/*.ts that exports a function, or when deciding between
name: convex-functions description: Writes Convex queries, mutations, actions, and internal functions in the object form with args and returns validators, correct ctx usage, runtime boundaries, and error handling. Use when adding or changing anything in convex/*.ts that exports a function, or when deciding between query, mutation, and action.
Every exported function in `convex/` uses the object form with `args` and `returns` validators. Pick the type by what the handler touches: queries read, mutations write, actions call out.
| Type | Database | External calls | Callable by | Use for | | --- | --- | --- | --- | --- | | `query` | Read | No | Clients, other functions | Reads. Cached and reactive. | | `mutation` | Read and write | No | Clients, other functions | Writes. One transaction. | | `action` | Only via `runQuery` and `runMutation` | Yes | Clients, scheduler, other actions | `fetch`, third party SDKs, Node APIs | | `internalQuery`, `internalMutation`, `internalAction` | Same as the public form | Same | Only other Convex functions | Scheduled work, crons, privileged writes | | `httpAction` | Only via `runQuery` and `runMutation` | Yes | HTTP requests in `convex/http.ts` | Webhooks, REST endpoints |
Default to query or mutation. Reach for an action only when the handler must talk to something outside Convex.
Declare `args` and `returns` on every function. A function that returns nothing declares `returns: v.null()` and returns `null`. Hoist a shared document validator when several functions return the same shape.
// convex/tasks.ts
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
const taskValidator = v.object({
_id: v.id("tasks"),
_creationTime: v.number(),
userId: v.id("users"),
title: v.string(),
completed: v.boolean(),
});
export const get = query({
args: { taskId: v.id("tasks") },
returns: v.union(taskValidator, v.null()),
handler: async (ctx, args) => {
return await ctx.db.get(args.taskId);
},
});
export const remove = mutation({
args: { taskId: v.id("tasks") },
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.delete(args.taskId);
return null;
},
});Use `ctx.db.get(id)` for one document by id. For everything else use `withIndex` against an index defined in `convex/schema.ts`. Never call `.filter()` on a table query; it scans the whole table.
export const listByUser = query({
args: { userId: v.id("users") },
returns: v.array(taskValidator),
handler: async (ctx, args) => {
return await ctx.db
.query("tasks")
.withIndex("by_user", (q) => q.eq("userId", args.userId))
.order("desc")
.take(50);
},
});Pick the terminal method by how many documents you expect:
| Method | Returns | Use when | | --- | --- | --- | | `.unique()` | One doc or null, throws on more than one | The index guarantees at most one match | | `.first()` | First doc or null | You want the newest or oldest match | | `.take(n)` | Up to n docs | A bounded list such as a recent feed | | `.collect()` | Every match | The result set is small and stays small | | `.paginate(opts)` | A page plus cursor | The table is unbounded |
Paginated queries take `paginationOpts: paginationOptsValidator` (from `convex/server`) as an argument.
| Method | What it does | | --- | --- | | `ctx.db.insert("tasks", doc)` | Inserts and returns the new id | | `ctx.db.patch(id, fields)` | Shallow merges fields. Throws if the doc is missing | | `ctx.db.replace(id, doc)` | Replaces the whole doc. Throws if missing | | `ctx.db.delete(id)` | Deletes the doc |
Patch directly when you do not need the old value. Reading first widens the window for write conflicts. Make mutations safe to retry.
export const rename = mutation({
args: { taskId: v.id("tasks"), title: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.patch(args.taskId, { title: args.title });
return null;
},
});`query`, `mutation`, and `action` are public. Anyone with the deployment URL can call them. Use `internalQuery`, `internalMutation`, and `internalAction` for code that should only run from other Convex code: scheduled jobs, crons, webhook handlers, privileged writes.
Reference functions through the generated objects in `./_generated/api`:
Always schedule `internal.*`. Scheduled functions and crons run without a client, so a public reference there skips the auth checks a client call would hit.
// convex/messages.ts
import { mutation, internalMutation } from "./_generated/server";
import { internal } from "./_generated/api";
import { v } from "convex/values";
export const send = mutation({
args: { channelId: v.id("channels"), content: v.string() },
returns: v.id("messages"),
handler: async (ctx, args) => {
const messageId = await ctx.db.insert("messages", args);
await ctx.scheduler.runAfter(0, internal.messages.notifySubscribers, {
channelId: args.channelId,
messageId,
});
return messageId;
},
});
export const notifySubscribers = internalMutation({
args: { channelId: v.id("channels"), messageId: v.id("messages") },
returns: v.null(),
handler: async (ctx, args) => {
const subs = await ctx.db
.query("subscriptions")
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
.collect();
await Promise.all(
subs.map((sub) =>
ctx.db.insert("notifications", {
userId: sub.userId,
messageId: args.messageId,
read: false,
}),
),
);Agent skills for builders shipping Convex apps. By Wayne Sutton. Seventeen skills.
Keeps a change scoped to what was asked. Flags extra features, refactors, and 'while we are here' edits, and moves them to a…
Builds AI agents on the Convex agent component: threads, messages, tools that call queries and mutations, streaming, RAG with…
Production patterns for Convex apps and the rules the @convex-dev/eslint-plugin enforces: validators, indexes, idempotent…
Creates reusable Convex components with defineComponent, a clean client wrapper, their own schema, and an npm publish setup. Use…
Schedules work in Convex: cron jobs in convex/crons.ts, one off scheduled functions with runAfter and runAt, batching large jobs,…
Handles files in Convex: upload URLs, storing blobs from actions, serving with getUrl or an HTTP action, metadata from the…