convex-actions

convex-actions

Best practices for Convex actions, transactions, and scheduling. Use when writing actions that call external APIs, using ctx.runQuery/ctx.runMutation, scheduling functions with ctx.scheduler, or working with the Convex runtime vs Node.js runtime ("use node").

0Star
0Fork
更新于 1/19/2026
SKILL.md
readonly只读
name
convex-actions
description

Best practices for Convex actions, transactions, and scheduling. Use when writing actions that call external APIs, using ctx.runQuery/ctx.runMutation, scheduling functions with ctx.scheduler, or working with the Convex runtime vs Node.js runtime ("use node").

Convex Actions

Function Types Overview

Type Database Access External APIs Caching Use Case
Query Read-only No Yes, reactive Fetching data
Mutation Read/Write No No Modifying data
Action Via runQuery/runMutation Yes No External integrations

Actions with Node.js Runtime

Add "use node"; at the top of files using Node.js APIs:

// convex/email.ts
"use node";

import { action, internalAction } from "./_generated/server";
import { v } from "convex/values";
import { internal } from "./_generated/api";

export const sendEmail = action({
  args: { to: v.string(), subject: v.string(), body: v.string() },
  returns: v.object({ success: v.boolean() }),
  handler: async (ctx, args) => {
    const apiKey = process.env.RESEND_API_KEY;
    if (!apiKey) throw new Error("RESEND_API_KEY not configured");

    const response = await fetch("https://api.resend.com/emails", {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${apiKey}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ from: "noreply@example.com", ...args }),
    });

    return { success: response.ok };
  },
});

Scheduling Functions

Use ctx.scheduler.runAfter to schedule functions:

export const createTask = mutation({
  args: { title: v.string(), userId: v.id("users") },
  returns: v.id("tasks"),
  handler: async (ctx, args) => {
    const taskId = await ctx.db.insert("tasks", {
      title: args.title,
      userId: args.userId,
      status: "pending",
    });

    // Schedule processing (always use internal functions!)
    await ctx.scheduler.runAfter(0, internal.tasks.processTask, { taskId });

    return taskId;
  },
});

// Internal function for scheduled work
export const processTask = internalMutation({
  args: { taskId: v.id("tasks") },
  returns: v.null(),
  handler: async (ctx, args) => {
    await ctx.db.patch("tasks", args.taskId, { status: "processing" });
    // ... processing logic
    return null;
  },
});

Use runAction Only When Changing Runtime

Replace runAction with plain TypeScript functions unless switching runtimes:

// Bad - unnecessary runAction overhead
await ctx.runAction(internal.scrape.scrapePage, { url });

// Good - plain TypeScript function
import * as Scrape from './model/scrape';
await Scrape.scrapePage(ctx, { url });

Avoid Sequential ctx.runMutation / ctx.runQuery

Each call runs in its own transaction. Combine for consistency:

// Bad - inconsistent reads
const team = await ctx.runQuery(internal.teams.getTeam, { teamId });
const owner = await ctx.runQuery(internal.teams.getOwner, { teamId });

// Good - single consistent query
const { team, owner } = await ctx.runQuery(internal.teams.getTeamAndOwner, { teamId });

// Bad - non-atomic loop
for (const user of users) {
  await ctx.runMutation(internal.users.insert, user);
}

// Good - atomic batch
await ctx.runMutation(internal.users.insertMany, { users });

Exceptions: Migrations, aggregations, or when side effects occur between calls.

Prefer Helper Functions in Queries/Mutations

Use plain TypeScript instead of ctx.runQuery/ctx.runMutation:

// Good - plain helper
import * as Users from './model/users';
const user = await Users.getCurrentUser(ctx);

// Bad - unnecessary overhead
const user = await ctx.runQuery(api.users.getCurrentUser);

Exception: Partial rollback needs ctx.runMutation:

try {
  await ctx.runMutation(internal.orders.process, { orderId });
} catch (e) {
  // Rollback process, record failure
  await ctx.db.insert("failures", { orderId, error: `${e}` });
}

Await All Promises

Always await async operations:

// Bad - missing await
ctx.scheduler.runAfter(0, internal.tasks.process, { id });
ctx.db.patch("tasks", docId, { status: "done" });

// Good - awaited
await ctx.scheduler.runAfter(0, internal.tasks.process, { id });
await ctx.db.patch("tasks", docId, { status: "done" });

ESLint: Use no-floating-promises rule.

Complete Action Example

// convex/payments.ts
"use node";

import { action, internalMutation } from "./_generated/server";
import { v } from "convex/values";
import { internal } from "./_generated/api";

export const processPayment = action({
  args: { orderId: v.id("orders"), amount: v.number() },
  returns: v.object({ success: v.boolean(), transactionId: v.optional(v.string()) }),
  handler: async (ctx, args) => {
    // 1. Read data via query
    const order = await ctx.runQuery(internal.orders.get, { orderId: args.orderId });
    if (!order) throw new Error("Order not found");

    // 2. Call external API
    const result = await fetch("https://api.stripe.com/v1/charges", {
      method: "POST",
      headers: { Authorization: `Bearer ${process.env.STRIPE_KEY}` },
      body: new URLSearchParams({ amount: String(args.amount * 100), currency: "usd" }),
    });
    const data = await result.json();

    // 3. Update database via mutation
    await ctx.runMutation(internal.orders.updateStatus, {
      orderId: args.orderId,
      status: data.status === "succeeded" ? "paid" : "failed",
      transactionId: data.id,
    });

    return { success: data.status === "succeeded", transactionId: data.id };
  },
});

References

You Might Also Like

Related Skills

coding-agent

coding-agent

179Kdev-codegen

Run Codex CLI, Claude Code, OpenCode, or Pi Coding Agent via background process for programmatic control.

openclaw avataropenclaw
获取
add-uint-support

add-uint-support

97Kdev-codegen

Add unsigned integer (uint) type support to PyTorch operators by updating AT_DISPATCH macros. Use when adding support for uint16, uint32, uint64 types to operators, kernels, or when user mentions enabling unsigned types, barebones unsigned types, or uint support.

pytorch avatarpytorch
获取
at-dispatch-v2

at-dispatch-v2

97Kdev-codegen

Convert PyTorch AT_DISPATCH macros to AT_DISPATCH_V2 format in ATen C++ code. Use when porting AT_DISPATCH_ALL_TYPES_AND*, AT_DISPATCH_FLOATING_TYPES*, or other dispatch macros to the new v2 API. For ATen kernel files, CUDA kernels, and native operator implementations.

pytorch avatarpytorch
获取
skill-writer

skill-writer

97Kdev-codegen

Guide users through creating Agent Skills for Claude Code. Use when the user wants to create, write, author, or design a new Skill, or needs help with SKILL.md files, frontmatter, or skill structure.

pytorch avatarpytorch
获取

Implements JavaScript classes in C++ using JavaScriptCore. Use when creating new JS classes with C++ bindings, prototypes, or constructors.

oven-sh avataroven-sh
获取

Creates JavaScript classes using Bun's Zig bindings generator (.classes.ts). Use when implementing new JS APIs in Zig with JSC integration.

oven-sh avataroven-sh
获取