Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use \"use node\" directive in action files that need Node.js APIs. Cannot write queries or mutations in \"use node\" files.
.claude/skills/kunanonj-cursor-plugin-convex-rule-use-node-for-actions/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 38% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 68% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 38% | 0% |
| case-17 | ✗→✓ | ▲ Improved | 65% | 0% |
| case-18 | ✗→✓ | ▲ Improved | 41% | 0% |
When you need Node.js APIs (fetch, crypto, Buffer, etc.) in Convex, you must use actions with the "use node" directive.
Files with "use node" can ONLY contain:
action functionsinternalAction functionsquery or mutation functionsFiles without "use node" can contain:
query functionsmutation functionsinternalQuery and internalMutation functionsUse actions with "use node" when you need:
typescript"use node"; import { action } from "./_generated/server"; import { v } from "convex/values"; export const fetchWeather = action({ args: { city: v.string() }, handler: async (ctx, args) => { // fetch is available because of "use node" const response = await fetch( `https://api.weather.com/weather?city=${args.city}` ); const data = await response.json(); // Store in database via mutation await ctx.runMutation(api.weather.store, { city: args.city, data: data, }); return data; }, });
typescript"use node"; import { action } from "./_generated/server"; import OpenAI from "openai"; const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); export const generateSuggestion = action({ args: { prompt: v.string() }, handler: async (ctx, args) => { const completion = await openai.chat.completions.create({ model: "gpt-4", messages: [{ role: "user", content: args.prompt }], }); return completion.choices[0].message.content; }, });
typescript"use node"; import { action } from "./_generated/server"; import crypto from "crypto"; export const generateSecureToken = action({ handler: async (ctx) => { const token = crypto.randomBytes(32).toString("hex"); await ctx.runMutation(api.tokens.store, { token }); return token; }, });
typescript"use node"; import { action } from "./_generated/server"; import Stripe from "stripe"; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); export const createPayment = action({ args: { amount: v.number() }, handler: async (ctx, args) => { const paymentIntent = await stripe.paymentIntents.create({ amount: args.amount, currency: "usd", }); return paymentIntent.client_secret; }, });
typescript"use node"; import { action, mutation } from "./_generated/server"; // ❌ ERROR: Cannot have mutations in "use node" file export const create = mutation({ handler: async (ctx, args) => { // This will fail! }, }); export const fetchData = action({ handler: async (ctx) => { const data = await fetch("..."); return data; }, });
convex/tasks.ts (no "use node"):
typescriptimport { query, mutation } from "./_generated/server"; export const list = query({ handler: async (ctx) => { return await ctx.db.query("tasks").collect(); }, }); export const create = mutation({ args: { title: v.string() }, handler: async (ctx, args) => { return await ctx.db.insert("tasks", { title: args.title }); }, });
convex/tasksActions.ts (with "use node"):
typescript"use node"; import { action } from "./_generated/server"; import { api } from "./_generated/api"; export const generateTaskSuggestions = action({ args: { userId: v.id("users") }, handler: async (ctx, args) => { // Fetch from external AI service const response = await fetch("https://ai-service.com/suggest", { method: "POST", body: JSON.stringify({ userId: args.userId }), }); const suggestions = await response.json(); // Store via mutation for (const suggestion of suggestions) { await ctx.runMutation(api.tasks.create, { title: suggestion.title, }); } return suggestions; }, });
Since actions can't directly modify the database in "use node" files, use this pattern:
typescript// convex/externalActions.ts "use node"; import { action } from "./_generated/server"; import { api, internal } from "./_generated/api"; export const syncFromExternalAPI = action({ handler: async (ctx) => { // 1. Fetch from external API (needs Node.js) const response = await fetch("https://api.example.com/data"); const data = await response.json(); // 2. Write to database via mutation await ctx.runMutation(internal.data.storeExternal, { data: data, }); }, }); // convex/data.ts (no "use node") import { internalMutation } from "./_generated/server"; export const storeExternal = internalMutation({ args: { data: v.any() }, handler: async (ctx, args) => { // Now we can write to database await ctx.db.insert("externalData", args.data); }, });
These work in regular queries/mutations without "use node":
typescript// convex/data.ts (no "use node" needed) import { action } from "./_generated/server"; export const fetchData = action({ handler: async (ctx) => { // Convex provides fetch in actions by default const response = await fetch("https://api.example.com/data"); return await response.json(); }, });
However, if you need Node.js-specific features like:
Then you need "use node".
| Need | Use | Directive | Can Write | |------|-----|-----------|-----------| | Database queries | query | No directive | queries only | | Database writes | mutation | No directive | mutations only | | External API | action | "use node" | actions only | | Node.js APIs | action | "use node" | actions only | | Third-party SDKs | action | "use node" | actions only |
Watch for these errors:
typescript"use node"; // ERROR: Cannot export mutations from "use node" files export const create = mutation({ ... });
typescript"use node"; // ERROR: Cannot export queries from "use node" files export const list = query({ ... });
typescript// ERROR: crypto is not available without "use node" import crypto from "crypto"; export const generate = action({ handler: async (ctx) => { const token = crypto.randomBytes(32); // Will fail! }, });
When writing Convex functions:
action with "use node"action with "use node"action with "use node"query (no "use node")mutation (no "use node")"use node"? → Only action exports"use node"| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-16 | pass→pass | 7,379 | 5,736 | -22% | 1 | 1 | 0% | 1,412 | 3,346 | +137% | 0 | 0 | — |
case-01 | fail→pass | 14,499 | 9,415 | -35% | 1 | 1 | 0% | 3,141 | 4,330 | +38% | 0 | 0 | — |
case-02 | fail→pass | 10,864 | 8,622 | -21% | 1 | 1 | 0% | 2,476 | 4,169 | +68% | 0 | 0 | — |
case-03 | pass→pass | 7,339 | 5,968 | -19% | 1 | 1 | 0% | 1,714 | 3,258 | +90% | 0 | 0 | — |
case-04 | pass→pass | 13,495 | 8,509 | -37% | 1 | 1 | 0% | 2,889 | 3,938 | +36% | 0 | 0 | — |
case-05 | fail→pass | 12,462 | 7,387 | -41% | 1 | 1 | 0% | 2,760 | 3,801 | +38% | 0 | 0 | — |
case-06 | pass→pass | 7,805 | 5,838 | -25% | 1 | 1 | 0% | 1,705 | 3,342 | +96% | 0 | 0 | — |
case-15 | pass→pass | 12,386 | 10,274 | -17% | 1 | 1 | 0% | 3,032 | 4,581 | +51% | 0 | 0 | — |
case-07 | pass→pass | 8,397 | 4,321 | -49% | 1 | 1 | 0% | 1,847 | 3,078 | +67% | 0 | 0 | — |
case-08 | pass→pass | 5,592 | 3,344 | -40% | 1 | 1 | 0% | 1,135 | 2,742 | +142% | 0 | 0 | — |
case-09 | pass→pass | 5,338 | 3,466 | -35% | 1 | 1 | 0% | 1,122 | 2,756 | +146% | 0 | 0 | — |
case-10 | pass→pass | 5,552 | 5,164 | -7% | 1 | 1 | 0% | 1,168 | 3,050 | +161% | 0 | 0 | — |
case-11 | pass→pass | 4,270 | 3,582 | -16% | 1 | 1 | 0% | 873 | 2,743 | +214% | 0 | 0 | — |
case-12 | pass→pass | 8,793 | 4,808 | -45% | 1 | 1 | 0% | 1,747 | 3,035 | +74% | 0 | 0 | — |
case-13 | pass→pass | 12,598 | 10,429 | -17% | 1 | 1 | 0% | 2,570 | 4,325 | +68% | 0 | 0 | — |
case-14 | pass→pass | 12,081 | 6,208 | -49% | 1 | 1 | 0% | 2,738 | 3,547 | +30% | 0 | 0 | — |
case-17 | fail→pass | 9,637 | 6,486 | -33% | 1 | 1 | 0% | 2,139 | 3,540 | +65% | 0 | 0 | — |
case-18 | fail→pass | 10,590 | 5,309 | -50% | 1 | 1 | 0% | 2,302 | 3,254 | +41% | 0 | 0 | — |
case-19 | pass→pass | 8,165 | 4,945 | -39% | 1 | 1 | 0% | 1,493 | 3,067 | +105% | 0 | 0 | — |
case-20 | pass→pass | 5,032 | 4,735 | -6% | 1 | 1 | 0% | 1,061 | 3,028 | +185% | 0 | 0 | — |
case-21 | pass→pass | 5,724 | 4,193 | -27% | 1 | 1 | 0% | 1,118 | 2,947 | +164% | 0 | 0 | — |
case-22 | pass→pass | 4,558 | 5,121 | +12% | 1 | 1 | 0% | 902 | 3,031 | +236% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +23 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.