Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Create Convex queries, mutations, and actions with proper validation, authentication, and error handling. Use when implementing new API endpoints.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 162% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 155% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 85% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 80% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 167% | 0% |
Generate secure, type-safe Convex functions following all best practices.
typescriptimport { query } from "./_generated/server"; import { v } from "convex/values"; export const getTask = query({ args: { taskId: v.id("tasks") }, returns: v.union(v.object({ _id: v.id("tasks"), text: v.string(), completed: v.boolean(), }), v.null()), handler: async (ctx, args) => { return await ctx.db.get(args.taskId); }, });
typescriptimport { mutation } from "./_generated/server"; import { v } from "convex/values"; export const createTask = mutation({ args: { text: v.string(), priority: v.optional(v.union( v.literal("low"), v.literal("medium"), v.literal("high") )), }, returns: v.id("tasks"), handler: async (ctx, args) => { const identity = await ctx.auth.getUserIdentity(); if (!identity) throw new Error("Not authenticated"); return await ctx.db.insert("tasks", { text: args.text, priority: args.priority ?? "medium", completed: false, createdAt: Date.now(), }); }, });
ctx.runMutation"use node" directive when needing Node.js APIsImportant: If your action needs Node.js-specific APIs (crypto, third-party SDKs, etc.), add "use node" at the top of the file. Files with "use node" can ONLY contain actions, not queries or mutations.
typescript"use node"; // Required for Node.js APIs like OpenAI SDK import { action } from "./_generated/server"; import { api } from "./_generated/api"; import { v } from "convex/values"; import OpenAI from "openai"; const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); export const generateTaskSuggestion = action({ args: { prompt: v.string() }, returns: v.string(), handler: async (ctx, args) => { const identity = await ctx.auth.getUserIdentity(); if (!identity) throw new Error("Not authenticated"); // Call OpenAI (requires "use node") const completion = await openai.chat.completions.create({ model: "gpt-4", messages: [{ role: "user", content: args.prompt }], }); const suggestion = completion.choices[0].message.content; // Write to database via mutation await ctx.runMutation(api.tasks.createTask, { text: suggestion, }); return suggestion; }, });
Note: If you only need basic fetch (no Node.js APIs), you can omit "use node". But for third-party SDKs, crypto, or other Node.js features, you must use it.
Always define args with validators:
typescriptargs: { id: v.id("tasks"), text: v.string(), count: v.number(), enabled: v.boolean(), tags: v.array(v.string()), metadata: v.optional(v.object({ key: v.string(), })), }
Always define returns:
typescriptreturns: v.object({ _id: v.id("tasks"), text: v.string(), }) // Or for arrays returns: v.array(v.object({ /* ... */ })) // Or for nullable returns: v.union(v.object({ /* ... */ }), v.null())
Always verify auth in public functions:
typescriptconst identity = await ctx.auth.getUserIdentity(); if (!identity) { throw new Error("Not authenticated"); }
Always verify ownership/permissions:
typescriptconst task = await ctx.db.get(args.taskId); if (!task) { throw new Error("Task not found"); } if (task.userId !== user._id) { throw new Error("Unauthorized"); }
typescriptexport const getMyTasks = query({ args: { status: v.optional(v.union( v.literal("active"), v.literal("completed") )), }, returns: v.array(v.object({ _id: v.id("tasks"), text: v.string(), completed: v.boolean(), })), handler: async (ctx, args) => { const identity = await ctx.auth.getUserIdentity(); if (!identity) throw new Error("Not authenticated"); const user = await ctx.db .query("users") .withIndex("by_token", q => q.eq("tokenIdentifier", identity.tokenIdentifier) ) .unique(); if (!user) throw new Error("User not found"); let query = ctx.db .query("tasks") .withIndex("by_user", q => q.eq("userId", user._id)); const tasks = await query.collect(); if (args.status) { return tasks.filter(t => args.status === "completed" ? t.completed : !t.completed ); } return tasks; }, });
typescriptexport const updateTask = mutation({ args: { taskId: v.id("tasks"), text: v.optional(v.string()), completed: v.optional(v.boolean()), }, returns: v.id("tasks"), handler: async (ctx, args) => { // 1. Authentication const identity = await ctx.auth.getUserIdentity(); if (!identity) throw new Error("Not authenticated"); // 2. Get user const user = await ctx.db .query("users") .withIndex("by_token", q => q.eq("tokenIdentifier", identity.tokenIdentifier) ) .unique(); if (!user) throw new Error("User not found"); // 3. Get resource const task = await ctx.db.get(args.taskId); if (!task) throw new Error("Task not found"); // 4. Authorization if (task.userId !== user._id) { throw new Error("Unauthorized"); } // 5. Update const updates: Partial<typeof task> = {}; if (args.text !== undefined) updates.text = args.text; if (args.completed !== undefined) updates.completed = args.completed; await ctx.db.patch(args.taskId, updates); return args.taskId; }, });
Create separate file for actions that need Node.js:
typescript// convex/taskActions.ts "use node"; // Required for SendGrid SDK import { action } from "./_generated/server"; import { api } from "./_generated/api"; import { v } from "convex/values"; import sendgrid from "@sendgrid/mail"; sendgrid.setApiKey(process.env.SENDGRID_API_KEY); export const sendTaskReminder = action({ args: { taskId: v.id("tasks") }, returns: v.boolean(), handler: async (ctx, args) => { // 1. Auth const identity = await ctx.auth.getUserIdentity(); if (!identity) throw new Error("Not authenticated"); // 2. Get data via query const task = await ctx.runQuery(api.tasks.getTask, { taskId: args.taskId, }); if (!task) throw new Error("Task not found"); // 3. Call external service (using Node.js SDK) await sendgrid.send({ to: identity.email, from: "noreply@example.com", subject: "Task Reminder", text: `Don't forget: ${task.text}`, }); // 4. Update via mutation await ctx.runMutation(api.tasks.markReminderSent, { taskId: args.taskId, }); return true; }, });
Note: Keep queries and mutations in convex/tasks.ts (without "use node"), and actions that need Node.js in convex/taskActions.ts (with "use node").
For backend-only functions (called by scheduler, other functions):
typescriptimport { internalMutation } from "./_generated/server"; export const processExpiredTasks = internalMutation({ args: {}, handler: async (ctx) => { // No auth needed - only callable from backend const now = Date.now(); const expired = await ctx.db .query("tasks") .withIndex("by_due_date", q => q.lt("dueDate", now)) .collect(); for (const task of expired) { await ctx.db.patch(task._id, { status: "expired" }); } }, });
args defined with validatorsreturns defined with validatorctx.auth.getUserIdentity()).filter() on queries)internal.* not api.*"use node" at top of file"use node": Only actions (no queries/mutations)Other measured skills in the registry, with their headline benchmark lift.