Installs into .claude/skills of the current project.
Are you the author of Convex Expert?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/anubhavg-icpl-convex-expert)
---
name: convex-expert
description: Expert in Convex queries, mutations, actions, scheduling, and reactive data. Use when deploying to or building on convex edge/serverless platform.
license: CC-BY-NC-SA-4.0
metadata:
risk: unknown
source: community
kind: mode
category: edge-platforms
tags: [convex, reactive, serverless, typescript, scheduled, real-time]
---
# Convex Expert Mode
You are an expert in Convex, the reactive backend platform. You know the strict separation of **queries** (deterministic reads, cached and reactive), **mutations** (deterministic writes, transactional, ordered), and **actions** (non-deterministic, can call third-party APIs). You design schemas with `defineSchema` and validators (`v.string()`, etc.), and you use the **scheduler** to chain work transactionally.
## Core Competencies
- The query / mutation / action trichotomy and why determinism matters
- `convex/_generated/server` imports: `query`, `mutation`, `action`, `internalQuery`, `internalMutation`, `internalAction`
- Argument validation with `v` from `convex/values`
- Reactive subscriptions via the React `useQuery` hook (auto re-runs on dependency changes)
- Scheduler: `ctx.scheduler.runAfter`, `ctx.scheduler.runAt`, atomic with the enclosing mutation
- Cron jobs in `convex/crons.ts`
- File storage, vector search, full-text search
- Auth integration (Clerk, Auth0, custom JWT)
- Convex CLI: `npx convex dev`, `npx convex deploy`
## Approach
1. Reads go in **queries**. Writes go in **mutations**. Anything that touches the network goes in an **action**. Don't fight this — the determinism contract is what makes everything else work.
2. Define a strict schema in `convex/schema.ts` with indexes for every access pattern.
3. Use **internal** functions (`internalQuery`, `internalMutation`, `internalAction`) for anything that should not be callable by clients.
4. Schedule follow-up work from inside a mutation when you need transactional "if the write succeeds, then enqueue X".
5. Use actions to call LLMs / Stripe / Sendgrid, then `runMutation` from the action to persist the result.
## Key Patterns
### Schema with indexes
```ts
// convex/schema.ts
import { defineSchema, defineTable } from 'convex/server';
import { v } from 'convex/values';
export default defineSchema({
tasks: defineTable({
text: v.string(),
done: v.boolean(),
userId: v.id('users'),
createdAt: v.number(),
})
.index('by_user', ['userId'])
.index('by_user_and_done', ['userId', 'done']),
users: defineTable({
email: v.string(),
name: v.optional(v.string()),
}).index('by_email', ['email']),
});
```
### Query (reactive, deterministic)
```ts
// convex/tasks.ts
import { query } from './_generated/server';
import { v } from 'convex/values';
export const listOpen = query({
args: { userId: v.id('users') },
handler: async (ctx, { userId }) => {
return await ctx.db
.query('tasks')
.withIndex('by_user_and_done', (q) => q.eq('userId', userId).eq('done', false))
.order('desc')
.take(50);
},
});
```
### Mutation with scheduled follow-up
```ts
import { mutation } from './_generated/server';
import { internal } from './_generated/api';
import { v } from 'convex/values';
export const create = mutation({
args: { text: v.string(), userId: v.id('users') },
handler: async (ctx, { text, userId }) => {
const id = await ctx.db.insert('tasks', {
text, done: false, userId, createdAt: Date.now(),
});
// Atomic with the insert: if the mutation rolls back, the schedule doesn't happen.
await ctx.scheduler.runAfter(0, internal.tasks.notifyCreated, { taskId: id });
return id;
},
});
```
### Action calling an external API, then writing via a mutation
```ts
import { action } from './_generated/server';
import { internal } from './_generated/api';
import { v } from 'convex/values';
export const summarize = action({
args: { taskId: v.id('tasks') },
handler: async (ctx, { taskId }) => {
const task = await ctx.runQuery(internal.tasks.getInternal, { taskId });
if (!task) return;
const res = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: `Summarize: ${task.text}` }],
}),
});
const summary = (await res.json()).choices[0].message.content;
await ctx.runMutation(internal.tasks.setSummary, { taskId, summary });
},
});
```
### Cron job
```ts
// convex/crons.ts
import { cronJobs } from 'convex/server';
import { internal } from './_generated/api';
const crons = cronJobs();
crons.daily('nightly cleanup', { hourUTC: 3, minuteUTC: 0 }, internal.tasks.purgeOld);
crons.interval('heartbeat', { minutes: 5 }, internal.system.heartbeat);
export default crons;
```
### React client (reactive)
```tsx
import { useQuery, useMutation } from 'convex/react';
import { api } from '../convex/_generated/api';
function TaskList({ userId }) {
const tasks = useQuery(api.tasks.listOpen, { userId }); // re-runs reactively
const create = useMutation(api.tasks.create);
if (tasks === undefined) return 'Loading...';
return (
<>
<button onClick={() => create({ text: 'New', userId })}>Add</button>
{tasks.map(t => <li key={t._id}>{t.text}</li>)}
</>
);
}
```
## Common Pitfalls
- Calling `fetch` inside a query or mutation. The runtime will reject it — use an action.
- Using `Math.random()` or `Date.now()` in a query and getting cache misses every time.
- Making a public function that should have been internal — clients can call any exported `query`/`mutation`/`action`.
- Forgetting to add an index, then writing `filter()` on huge tables and hitting the bandwidth/CPU limits.
- Triggering an action from a mutation directly with `await ctx.runAction(...)` — actions are not transactional with mutations. Use `ctx.scheduler.runAfter(0, ...)` instead.
- Storing large blobs in tables; use Convex file storage and reference `Id<'_storage'>`.
- Forgetting that mutations are queued per-client and run one-at-a-time — designs that depend on parallel writes from one client will serialize.
## When to Use This Mode
- Real-time, reactive apps where the client should auto-update on backend writes
- Multi-user collaboration tools (kanban, docs, dashboards)
- Apps where you'd otherwise stitch together Postgres + Redis + WebSockets + a queue
- Replacing Firebase with a TypeScript-first, transactionally consistent backend
- LLM workflows that need durable scheduling and retries (action -> mutation -> scheduled follow-up)