Mutations

Actions

Execute Convex actions for third-party API calls, long computations, and side effects.

Actions differ from mutations: they can call external APIs, run longer computations, and perform side effects that aren't possible in queries or mutations.

Basic Usage

<script setup lang="ts">
import { api } from '~/convex/_generated/api'

const { execute: sendEmail, pending, status, error } = useConvexAction(
  api.emails.send
)

async function handleSend() {
  try {
    await sendEmail({
      to: 'user@example.com',
      subject: 'Welcome!',
      body: 'Thanks for signing up.',
    })
  } catch {
    // error is automatically tracked
  }
}
</script>

<template>
  <button :disabled="pending" @click="handleSend">
    {{ pending ? 'Sending...' : 'Send Email' }}
  </button>
  <p v-if="status === 'error'" class="error">{{ error?.message }}</p>
  <p v-if="status === 'success'" class="success">Email sent!</p>
</template>

API Reference

Parameters

ParameterTypeDescription
actionFunctionReference<"action">Convex action function reference
optionsUseConvexActionOptionsOptional configuration
Enable module-level logging via the logging config option for debugging actions.

Returns

PropertyTypeDescription
execute(args) => Promise<Result>Execute the action
dataRef<Result | undefined>Result from last successful action
statusComputedRef<ActionStatus>'idle' | 'pending' | 'success' | 'error'
pendingComputedRef<boolean>True while action is in flight
errorRef<Error | null>Error from last attempt
reset() => voidClear error and status

When to Use Actions

Use actions when you need to:

Use CaseExample
Call external APIsPayment processing, email sending, AI services
Long computationsData processing, file generation, reports
Non-deterministic operationsGenerating random values, getting current time
Side effectsSending notifications, logging to external services
Actions run in a Node.js environment and can use fetch, npm packages, and other Node APIs. They don't have direct database access like mutations.

Patterns

API Integration

<script setup lang="ts">
const { execute: generateImage, pending, data: imageUrl } = useConvexAction(
  api.ai.generateImage
)

const prompt = ref('')

async function handleGenerate() {
  await generateImage({ prompt: prompt.value })
}
</script>

<template>
  <div>
    <input v-model="prompt" placeholder="Describe an image..." />
    <button :disabled="pending" @click="handleGenerate">
      {{ pending ? 'Generating...' : 'Generate' }}
    </button>

    <img v-if="imageUrl" :src="imageUrl" alt="Generated image" />
  </div>
</template>

File Processing

<script setup lang="ts">
const { execute: processFile, pending, status, data } = useConvexAction(
  api.files.process
)

async function handleUpload(file: File) {
  const base64 = await fileToBase64(file)
  await processFile({ fileName: file.name, content: base64 })
}
</script>

<template>
  <input type="file" @change="handleUpload($event.target.files[0])" />
  <div v-if="pending">Processing file...</div>
  <div v-if="status === 'success'">
    Processed! Result: {{ data }}
  </div>
</template>

Payment Processing

<script setup lang="ts">
const { execute: createCheckout, pending, error } = useConvexAction(
  api.payments.createCheckoutSession
)

async function handleCheckout() {
  try {
    const { url } = await createCheckout({
      priceId: 'price_xxx',
      successUrl: window.location.origin + '/success',
      cancelUrl: window.location.origin + '/cancel',
    })
    // Redirect to Stripe
    window.location.href = url
  } catch {
    // Show error
  }
}
</script>

<template>
  <button :disabled="pending" @click="handleCheckout">
    {{ pending ? 'Redirecting...' : 'Checkout' }}
  </button>
  <p v-if="error" class="error">{{ error.message }}</p>
</template>

With Status Display

<script setup lang="ts">
const { execute, status, data, error, reset } = useConvexAction(
  api.reports.generate
)
</script>

<template>
  <div class="status-card">
    <template v-if="status === 'idle'">
      <button @click="execute({ month: 'january' })">
        Generate Report
      </button>
    </template>

    <template v-else-if="status === 'pending'">
      <div class="spinner" />
      <p>Generating report...</p>
    </template>

    <template v-else-if="status === 'success'">
      <p>Report ready!</p>
      <a :href="data.downloadUrl" download>Download</a>
      <button @click="reset">Generate Another</button>
    </template>

    <template v-else-if="status === 'error'">
      <p class="error">Failed: {{ error?.message }}</p>
      <button @click="reset">Try Again</button>
    </template>
  </div>
</template>

Comparison with Mutations

AspectuseConvexMutationuseConvexAction
Database accessDirect via ctx.dbVia internal mutations
External APIsNot allowedAllowed
Execution timeShould be fastCan be longer
Optimistic updatesSupportedNot supported
Use caseData changesSide effects, integrations

Example: Using Both

<script setup lang="ts">
// Mutation for database changes
const { mutate: saveOrder } = useConvexMutation(api.orders.create)

// Action for external API call
const { execute: chargeCard } = useConvexAction(api.payments.charge)

async function handlePurchase() {
  // First, charge the card (action - calls Stripe)
  const { transactionId } = await chargeCard({
    amount: total.value,
    token: cardToken.value,
  })

  // Then, save the order (mutation - writes to database)
  await saveOrder({
    items: cart.value,
    transactionId,
  })
}
</script>

TypeScript

Full type inference from your Convex schema:

// Args and return type are inferred
const { execute, data } = useConvexAction(api.emails.send)

// Type error if args don't match
await execute({ wrong: 'args' })

// data is typed based on action return
console.log(data.value?.messageId) // typed correctly

Common Mistakes

1. Using actions for database operations

// WRONG: Use mutation for database writes
const { execute } = useConvexAction(api.users.updateProfile)

// RIGHT: Use mutation
const { mutate } = useConvexMutation(api.users.updateProfile)

2. Expecting real-time updates

// Actions don't trigger query subscriptions automatically
// If your action affects data, call a mutation to update the database

// In your Convex action:
export const processAndSave = action({
  handler: async (ctx, args) => {
    const result = await processExternally(args)
    // Call mutation to save and trigger subscriptions
    await ctx.runMutation(internal.data.save, { result })
  },
})

3. Not handling long-running operations

<!-- WRONG: No feedback for long operations -->
<template>
  <button @click="execute({ data: largeData })">Process</button>
</template>

<!-- RIGHT: Show progress -->
<template>
  <button :disabled="pending" @click="execute({ data: largeData })">
    <span v-if="pending">Processing... This may take a minute</span>
    <span v-else>Process Data</span>
  </button>
</template>