skills/get-convex/agent-skills/function-creator

function-creator

SKILL.md

Convex Function Creator

Generate secure, type-safe Convex functions following all best practices.

When to Use

  • Creating new query functions (read data)
  • Creating new mutation functions (write data)
  • Creating new action functions (external APIs, long-running)
  • Adding API endpoints to your Convex backend

Function Types

Queries (Read-Only)

  • Can only read from database
  • Cannot modify data or call external APIs
  • Cached and reactive
  • Run in transactions
import { 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);
  },
});

Mutations (Transactional Writes)

  • Can read and write to database
  • Cannot call external APIs
  • Run in ACID transactions
  • Automatic retries on conflicts
import { 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(),
    });
  },
});

Actions (External + Non-Transactional)

  • Can call external APIs (fetch, AI, etc.)
  • Can call mutations via ctx.runMutation
  • Cannot directly access database
  • No automatic retries
  • Use "use node" directive when needing Node.js APIs

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

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

Required Components

1. Argument Validation

Always define args with validators:

args: {
  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(),
  })),
}

2. Return Type Validation

Always define returns:

returns: 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())

3. Authentication Check

Always verify auth in public functions:

const identity = await ctx.auth.getUserIdentity();
if (!identity) {
  throw new Error("Not authenticated");
}

4. Authorization Check

Always verify ownership/permissions:

const task = await ctx.db.get(args.taskId);
if (!task) {
  throw new Error("Task not found");
}

if (task.userId !== user._id) {
  throw new Error("Unauthorized");
}

Complete Examples

Secure Query with Auth

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

Secure Mutation with Validation

export 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<any> = {};
    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;
  },
});

Action Calling External API

Create separate file for actions that need Node.js:

// 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").

Internal Functions

For backend-only functions (called by scheduler, other functions):

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

Checklist

  • args defined with validators
  • returns defined with validator
  • Authentication check (ctx.auth.getUserIdentity())
  • Authorization check (ownership/permissions)
  • All promises awaited
  • Indexed queries (no .filter() on queries)
  • Error handling with descriptive messages
  • Scheduled functions use internal.* not api.*
  • If using Node.js APIs: "use node" at top of file
  • If file has "use node": Only actions (no queries/mutations)
  • Actions in separate file from queries/mutations when using "use node"
Weekly Installs
550
GitHub Stars
7
First Seen
Feb 18, 2026
Installed on
github-copilot545
opencode544
codex544
amp544
kimi-cli544
gemini-cli544