Actions differ from mutations: they can call external APIs, run longer computations, and perform side effects that aren't possible in queries or mutations.
<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>
| Parameter | Type | Description |
|---|---|---|
action | FunctionReference<"action"> | Convex action function reference |
options | UseConvexActionOptions | Optional configuration |
| Property | Type | Description |
|---|---|---|
execute | (args) => Promise<Result> | Execute the action |
data | Ref<Result | undefined> | Result from last successful action |
status | ComputedRef<ActionStatus> | 'idle' | 'pending' | 'success' | 'error' |
pending | ComputedRef<boolean> | True while action is in flight |
error | Ref<Error | null> | Error from last attempt |
reset | () => void | Clear error and status |
Use actions when you need to:
| Use Case | Example |
|---|---|
| Call external APIs | Payment processing, email sending, AI services |
| Long computations | Data processing, file generation, reports |
| Non-deterministic operations | Generating random values, getting current time |
| Side effects | Sending notifications, logging to external services |
fetch, npm packages, and other Node APIs. They don't have direct database access like mutations.<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>
<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>
<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>
<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>
| Aspect | useConvexMutation | useConvexAction |
|---|---|---|
| Database access | Direct via ctx.db | Via internal mutations |
| External APIs | Not allowed | Allowed |
| Execution time | Should be fast | Can be longer |
| Optimistic updates | Supported | Not supported |
| Use case | Data changes | Side effects, integrations |
<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>
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
// WRONG: Use mutation for database writes
const { execute } = useConvexAction(api.users.updateProfile)
// RIGHT: Use mutation
const { mutate } = useConvexMutation(api.users.updateProfile)
// 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 })
},
})
<!-- 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>