Skip to main content
Actions allow you to call external APIs, use Node.js libraries, and perform non-deterministic operations. Unlike queries and mutations, actions do not have direct database access.

Defining actions

action

Define a public action function that can be called from clients.
import { action } from "./_generated/server";
import { internal } from "./_generated/api";
import { v } from "convex/values";

export const sendWelcomeEmail = action({
  args: { userId: v.id("users") },
  returns: v.null(),
  handler: async (ctx, args) => {
    // Read data via ctx.runQuery
    const user = await ctx.runQuery(internal.users.get, { id: args.userId });
    
    // Call external API
    const response = await fetch("https://api.sendgrid.com/v3/mail/send", {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${process.env.SENDGRID_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        personalizations: [{ to: [{ email: user.email }] }],
        from: { email: "[email protected]" },
        subject: "Welcome!",
        content: [{ type: "text/plain", value: `Welcome ${user.name}!` }],
      }),
    });
    
    if (!response.ok) {
      throw new Error(`Failed to send email: ${response.statusText}`);
    }
    
    return null;
  },
});
args
PropertyValidators
Argument validation object using validators from convex/values.
returns
Validator
Return value validator.
handler
function
required
The implementation function that receives an ActionCtx and validated arguments.
ctx
ActionCtx
Action context without direct database access.
args
object
Validated arguments matching the args validator.

internalAction

Define an internal action that can only be called from other Convex functions.
import { internalAction } from "./_generated/server";
import { internal } from "./_generated/api";
import { v } from "convex/values";

export const processPayment = internalAction({
  args: {
    orderId: v.id("orders"),
    amount: v.number(),
  },
  returns: v.object({
    success: v.boolean(),
    transactionId: v.optional(v.string()),
  }),
  handler: async (ctx, args) => {
    const order = await ctx.runQuery(internal.orders.get, { id: args.orderId });
    
    // Call payment processor
    const result = await fetch("https://api.stripe.com/v1/charges", {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${process.env.STRIPE_SECRET_KEY}`,
      },
      body: new URLSearchParams({
        amount: String(args.amount),
        currency: "usd",
        source: order.paymentToken,
      }),
    });
    
    const data = await result.json();
    
    if (result.ok) {
      await ctx.runMutation(internal.orders.markPaid, {
        orderId: args.orderId,
        transactionId: data.id,
      });
      return { success: true, transactionId: data.id };
    }
    
    return { success: false };
  },
});

Action context

ActionCtx

The context object passed to action handlers.
runQuery
function
Run a Convex query. Each call is a separate read transaction.
const user = await ctx.runQuery(internal.users.get, { userId });
Tip: Use internalQuery to prevent users from calling the query directly.
runMutation
function
Run a Convex mutation. Each call is a separate write transaction.
await ctx.runMutation(internal.orders.markPaid, { orderId });
Tip: Use internalMutation to prevent users from calling it directly.
runAction
function
Run another Convex action.
await ctx.runAction(internal.emails.send, { userId });
Important: Only use this when crossing runtimes (e.g., calling a "use node" action from the default runtime). For code in the same runtime, extract shared logic into a TypeScript helper function instead.
scheduler
Scheduler
Schedule functions to run in the future.
// Schedule a reminder for later
await ctx.scheduler.runAfter(
  7 * 24 * 60 * 60 * 1000, // 7 days
  internal.reminders.send,
  { userId }
);
See Scheduler API for details.
auth
Auth
Authentication interface to get the current user’s identity.
const identity = await ctx.auth.getUserIdentity();
storage
StorageActionWriter
File storage interface with additional methods available only in actions.
// Download a file as a Blob
const blob = await ctx.storage.get(storageId);

// Upload a Blob directly
const newStorageId = await ctx.storage.store(blob);
See Storage API for details.
Run a vector search on a table.
const results = await ctx.vectorSearch("documents", "by_embedding", {
  vector: embedding,
  limit: 10,
});

Common use cases

Calling external APIs

Actions can make HTTP requests to external services:
export const fetchWeather = action({
  args: { city: v.string() },
  returns: v.object({
    temperature: v.number(),
    condition: v.string(),
  }),
  handler: async (ctx, args) => {
    const response = await fetch(
      `https://api.weather.com/v1/current?city=${args.city}&key=${process.env.WEATHER_API_KEY}`
    );
    return await response.json();
  },
});

Using Node.js libraries

Actions can use Node.js built-in modules and npm packages:
import { action } from "./_generated/server";
import { v } from "convex/values";
import crypto from "crypto";

export const generateToken = action({
  args: {},
  returns: v.string(),
  handler: async (ctx, args) => {
    return crypto.randomBytes(32).toString("hex");
  },
});

Processing files

Actions can download, process, and re-upload files:
import { action } from "./_generated/server";
import { internal } from "./_generated/api";
import { v } from "convex/values";
import sharp from "sharp";

export const generateThumbnail = action({
  args: { imageId: v.id("_storage") },
  returns: v.id("_storage"),
  handler: async (ctx, args) => {
    // Download the image
    const blob = await ctx.storage.get(args.imageId);
    if (!blob) throw new Error("Image not found");
    
    // Process with sharp
    const buffer = Buffer.from(await blob.arrayBuffer());
    const thumbnail = await sharp(buffer)
      .resize(200, 200)
      .toBuffer();
    
    // Upload the thumbnail
    const thumbnailBlob = new Blob([thumbnail], { type: "image/jpeg" });
    const thumbnailId = await ctx.storage.store(thumbnailBlob);
    
    return thumbnailId;
  },
});

Execution guarantees

At most once execution

Unlike mutations, actions are not automatically retried on transient errors. They execute at most once.

No direct database access

Actions cannot use ctx.db. Use ctx.runQuery and ctx.runMutation instead.

Non-deterministic operations allowed

Actions can call external APIs, use randomness, access the current time, and perform other non-deterministic operations.

Best practices

Use internal actions

For actions that should only be called from other functions, use internalAction.

Handle errors gracefully

Actions can fail due to network issues or external API errors. Always handle errors appropriately.

Keep actions idempotent

When possible, design actions so they can be safely retried without side effects.

Don't use runAction unnecessarily

Only use runAction when crossing runtimes. For shared logic in the same runtime, use helper functions.

Build docs developers (and LLMs) love