Skip to content
Backend
Skill

/convex-functions

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

BOOST
From plugin
builder-skills
40619 skills
Install
$ npx -y skills add waynesutton/builder-skills --skill convex-functions --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/convex-functions

Context 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

SKILL.md

convex-functions.SKILL.md
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.

Convex functions

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.

Pick the function type

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

The object form

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;
  },
});

Reading data

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.

Writing data

| 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;
  },
});

Internal functions and references

`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`:

  • `api.tasks.get` points at a public function in `convex/tasks.ts`
  • `internal.tasks.markPaid` points at an internal function in the same file
  • Folders map to paths: `convex/billing/invoices.ts` gives `api.billing.invoices.list`

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,
        }),
      ),
    );
Read more
Ships withbuilder-skills

Agent skills for builders shipping Convex apps. By Wayne Sutton. Seventeen skills.

Get the whole plugin

Other skills on builder-skills.