Expert in Convex queries, mutations, actions, scheduling, and reactive data. Use when deploying to or building on convex edge/serverless platform.
Scanned 9/8/2026
Install to Claude Code
npx -y skills add anubhavg-icpl/vibe --skill convex-expert --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Convex Expert?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/anubhavg-icpl-convex-expert)More formats (shields.io, HTML) on the badges page.
---
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)
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!