{"id":"convex-functions","name":"convex-functions","summary":"クエリ、ミューテーション、アクション、HTTPアクションを適切に掘り下げ、エラー処理、内部関数、ランタイムを考慮しながら書くこと","body":"# Convex Functions\n\nMaster Convex functions including queries, mutations, actions, and HTTP endpoints with proper validation, error handling, and runtime considerations.\n\n## Code Quality\n\nAll examples in this skill comply with @convex-dev/eslint-plugin rules:\n\n- Object syntax with `handler` property\n- Argument validators on all functions\n- Explicit table names in database operations\n\nSee the Code Quality section in [convex-best-practices](../convex-best-practices/SKILL.md) for linting setup.\n\n## Documentation Sources\n\nBefore implementing, do not assume; fetch the latest documentation:\n\n- Primary: https://docs.convex.dev/functions\n- Query Functions: https://docs.convex.dev/functions/query-functions\n- Mutation Functions: https://docs.convex.dev/functions/mutation-functions\n- Actions: https://docs.convex.dev/functions/actions\n- HTTP Actions: https://docs.convex.dev/functions/http-actions\n- For broader context: https://docs.convex.dev/llms.txt\n\n## Instructions\n\n### Function Types Overview\n\n| Type        | Database Access          | External APIs | Caching       | Use Case              |\n| ----------- | ------------------------ | ------------- | ------------- | --------------------- |\n| Query       | Read-only                | No            | Yes, reactive | Fetching data         |\n| Mutation    | Read/Write               | No            | No            | Modifying data        |\n| Action      | Via runQuery/runMutation | Yes           | No            | External integrations |\n| HTTP Action | Via runQuery/runMutation | Yes           | No            | Webhooks, APIs        |\n\n### Queries\n\nQueries are reactive, cached, and read-only:\n\n```typescript\nimport { query } from \"./_generated/server\";\nimport { v } from \"convex/values\";\n\nexport const getUser = query({\n  args: { userId: v.id(\"users\") },\n  returns: v.union(\n    v.object({\n      _id: v.id(\"users\"),\n      _creationTime: v.number(),\n      name: v.string(),\n      email: v.string(),\n    }),\n    v.null(),\n  ),\n  handler: async (ctx, args) => {\n    return await ctx.db.get(\"users\", args.userId);\n  },\n});\n\n// Query with index\nexport const listUserTasks = query({\n  args: { userId: v.id(\"users\") },\n  returns: v.array(\n    v.object({\n      _id: v.id(\"tasks\"),\n      _creationTime: v.number(),\n      title: v.string(),\n      completed: v.boolean(),\n    }),\n  ),\n  handler: async (ctx, args) => {\n    return await ctx.db\n      .query(\"tasks\")\n      .withIndex(\"by_user\", (q) => q.eq(\"userId\", args.userId))\n      .order(\"desc\")\n      .collect();\n  },\n});\n```\n\n### Mutations\n\nMutations modify the database and are transactional:\n\n```typescript\nimport { mutation } from \"./_generated/server\";\nimport { v } from \"convex/values\";\nimport { ConvexError } from \"convex/values\";\n\nexport const createTask = mutation({\n  args: {\n    title: v.string(),\n    userId: v.id(\"users\"),\n  },\n  returns: v.id(\"tasks\"),\n  handler: async (ctx, args) => {\n    // Validate user exists\n    const user = await ctx.db.get(\"users\", args.userId);\n    if (!user) {\n      throw new ConvexError(\"User not found\");\n    }\n\n    return await ctx.db.insert(\"tasks\", {\n      title: args.title,\n      userId: args.userId,\n      completed: false,\n      createdAt: Date.now(),\n    });\n  },\n});\n\nexport const deleteTask = mutation({\n  args: { taskId: v.id(\"tasks\") },\n  returns: v.null(),\n  handler: async (ctx, args) => {\n    await ctx.db.delete(\"tasks\", args.taskId);\n    return null;\n  },\n});\n```\n\n### Actions\n\nActions can call external APIs but have no direct database access:\n\n```typescript\n\"use node\";\n\nimport { action } from \"./_generated/server\";\nimport { v } from \"convex/values\";\nimport { api, internal } from \"./_generated/api\";\n\nexport const sendEmail = action({\n  args: {\n    to: v.string(),\n    subject: v.string(),\n    body: v.string(),\n  },\n  returns: v.object({ success: v.boolean() }),\n  handler: async (ctx, args) => {\n    // Call external API\n    const response = await fetch(\"https://api.email.com/send\", {\n      method: \"POST\",\n      headers: { \"Content-Type\": \"application/json\" },\n      body: JSON.stringify(args),\n    });\n\n    return { success: response.ok };\n  },\n});\n\n// Action calling queries and mutations\nexport const processOrder = action({\n  args: { orderId: v.id(\"orders\") },\n  returns: v.null(),\n  handler: async (ctx, args) => {\n    // Read data via query\n    const order = await ctx.runQuery(api.orders.get, { orderId: args.orderId });\n\n    if (!order) {\n      throw new Error(\"Order not found\");\n    }\n\n    // Call external payment API\n    const paymentResult = await processPayment(order);\n\n    // Update database via mutation\n    await ctx.runMutation(internal.orders.updateStatus, {\n      orderId: args.orderId,\n      status: paymentResult.success ? \"paid\" : \"failed\",\n    });\n\n    return null;\n  },\n});\n```\n\n### HTTP Actions\n\nHTTP actions handle webhooks and external requests:\n\n```typescript\n// convex/http.ts\nimport { httpRouter } from \"convex/server\";\nimport { httpAction } from \"./_generated/server\";\nimport { api, internal } from \"./_generated/api\";\n\nconst http = httpRouter();\n\n// Webhook endpoint\nhttp.route({\n  path: \"/webhooks/stripe\",\n  method: \"POST\",\n  handler: httpAction(async (ctx, request) => {\n    const signature = request.headers.get(\"stripe-signature\");\n    const body = await request.text();\n\n    // Verify webhook signature\n    if (!verifyStripeSignature(body, signature)) {\n      return new Response(\"Invalid signature\", { status: 401 });\n    }\n\n    const event = JSON.parse(body);\n\n    // Process webhook\n    await ctx.runMutation(internal.payments.handleWebhook, {\n      eventType: event.type,\n      data: event.data,\n    });\n\n    return new Response(\"OK\", { status: 200 });\n  }),\n});\n\n// API endpoint\nhttp.route({\n  path: \"/api/users/:userId\",\n  method: \"GET\",\n  handler: httpAction(async (ctx, request) => {\n    const url = new URL(request.url);\n    const userId = url.pathname.split(\"/\").pop();\n\n    const user = await ctx.runQuery(api.users.get, {\n      userId: userId as Id<\"users\">,\n    });\n\n    if (!user) {\n      return new Response(\"Not found\", { status: 404 });\n    }\n\n    return Response.json(user);\n  }),\n});\n\nexport default http;\n```\n\n### Internal Functions\n\nUse internal functions for sensitive operations:\n\n```typescript\nimport {\n  internalMutation,\n  internalQuery,\n  internalAction,\n} from \"./_generated/server\";\nimport { v } from \"convex/values\";\n\n// Only callable from other Convex functions\nexport const _updateUserCredits = internalMutation({\n  args: {\n    userId: v.id(\"users\"),\n    amount: v.number(),\n  },\n  returns: v.null(),\n  handler: async (ctx, args) => {\n    const user = await ctx.db.get(\"users\", args.userId);\n    if (!user) return null;\n\n    await ctx.db.patch(\"users\", args.userId, {\n      credits: (user.credits || 0) + args.amount,\n    });\n    return null;\n  },\n});\n\n// Call internal function from action\nexport const purchaseCredits = action({\n  args: { userId: v.id(\"users\"), amount: v.number() },\n  returns: v.null(),\n  handler: async (ctx, args) => {\n    // Process payment externally\n    await processPayment(args.amount);\n\n    // Update credits via internal mutation\n    await ctx.runMutation(internal.users._updateUserCredits, {\n      userId: args.userId,\n      amount: args.amount,\n    });\n\n    return null;\n  },\n});\n```\n\n### Scheduling Functions\n\nSchedule functions to run later:\n\n```typescript\nimport { mutation, internalMutation } from \"./_generated/server\";\nimport { v } from \"convex/values\";\nimport { internal } from \"./_generated/api\";\n\nexport const scheduleReminder = mutation({\n  args: {\n    userId: v.id(\"users\"),\n    message: v.string(),\n    delayMs: v.number(),\n  },\n  returns: v.id(\"_scheduled_functions\"),\n  handler: async (ctx, args) => {\n    return await ctx.scheduler.runAfter(\n      args.delayMs,\n      internal.notifications.sendReminder,\n      { userId: args.userId, message: args.message },\n    );\n  },\n});\n\nexport const sendReminder = internalMutation({\n  args: {\n    userId: v.id(\"users\"),\n    message: v.string(),\n  },\n  returns: v.null(),\n  handler: async (ctx, args) => {\n    await ctx.db.insert(\"notifications\", {\n      userId: args.userId,\n      message: args.message,\n      sentAt: Date.now(),\n    });\n    return null;\n  },\n});\n```\n\n## Examples\n\n### Complete Function File\n\n```typescript\n// convex/messages.ts\nimport { query, mutation, internalMutation } from \"./_generated/server\";\nimport { v } from \"convex/values\";\nimport { ConvexError } from \"convex/values\";\nimport { internal } from \"./_generated/api\";\n\nconst messageValidator = v.object({\n  _id: v.id(\"messages\"),\n  _creationTime: v.number(),\n  channelId: v.id(\"channels\"),\n  authorId: v.id(\"users\"),\n  content: v.string(),\n  editedAt: v.optional(v.number()),\n});\n\n// Public query\nexport const list = query({\n  args: {\n    channelId: v.id(\"channels\"),\n    limit: v.optional(v.number()),\n  },\n  returns: v.array(messageValidator),\n  handler: async (ctx, args) => {\n    const limit = args.limit ?? 50;\n    return await ctx.db\n      .query(\"messages\")\n      .withIndex(\"by_channel\", (q) => q.eq(\"channelId\", args.channelId))\n      .order(\"desc\")\n      .take(limit);\n  },\n});\n\n// Public mutation\nexport const send = mutation({\n  args: {\n    channelId: v.id(\"channels\"),\n    authorId: v.id(\"users\"),\n    content: v.string(),\n  },\n  returns: v.id(\"messages\"),\n  handler: async (ctx, args) => {\n    if (args.content.trim().length === 0) {\n      throw new ConvexError(\"Message cannot be empty\");\n    }\n\n    const messageId = await ctx.db.insert(\"messages\", {\n      channelId: args.channelId,\n      authorId: args.authorId,\n      content: args.content.trim(),\n    });\n\n    // Schedule notification\n    await ctx.scheduler.runAfter(0, internal.messages.notifySubscribers, {\n      channelId: args.channelId,\n      messageId,\n    });\n\n    return messageId;\n  },\n});\n\n// Internal mutation\nexport const notifySubscribers = internalMutation({\n  args: {\n    channelId: v.id(\"channels\"),\n    messageId: v.id(\"messages\"),\n  },\n  returns: v.null(),\n  handler: async (ctx, args) => {\n    // Get channel subscribers and notify them\n    const subscribers = await ctx.db\n      .query(\"subscriptions\")\n      .withIndex(\"by_channel\", (q) => q.eq(\"channelId\", args.channelId))\n      .collect();\n\n    for (const sub of subscribers) {\n      await ctx.db.insert(\"notifications\", {\n        userId: sub.userId,\n        messageId: args.messageId,\n        read: false,\n      });\n    }\n    return null;\n  },\n});\n```\n\n## Best Practices\n\n- Never run `npx convex deploy` unless explicitly instructed\n- Never run any git commands unless explicitly instructed\n- Always define args and returns validators\n- Use queries for read operations (they are cached and reactive)\n- Use mutations for write operations (they are transactional)\n- Use actions only when calling external APIs\n- Use internal functions for sensitive operations\n- Add `\"use node\";` at the top of action files using Node.js APIs\n- Handle errors with ConvexError for user-facing messages\n\n## Common Pitfalls\n\n1. **Using actions for database operations** - Use queries/mutations instead\n2. **Calling external APIs from queries/mutations** - Use actions\n3. **Forgetting to add \"use node\"** - Required for Node.js APIs in actions\n4. **Missing return validators** - Always specify returns\n5. **Not using internal functions for sensitive logic** - Protect with internalMutation\n\n## References\n\n- Convex Documentation: https://docs.convex.dev/\n- Convex LLMs.txt: https://docs.convex.dev/llms.txt\n- Functions Overview: https://docs.convex.dev/functions\n- Query Functions: https://docs.convex.dev/functions/query-functions\n- Mutation Functions: https://docs.convex.dev/functions/mutation-functions\n- Actions: https://docs.convex.dev/functions/actions","author":"@waynesutton","ownerProfile":null,"authorContacts":null,"sourceUrl":"https://github.com/waynesutton/convexskills/tree/main/skills/convex-functions","license":"Apache-2.0","category":"writing","lang":"en","tokens":2807,"stars":0,"calls30d":2,"claimed":false,"visibility":"public","origin":"crawler","version":"0.1.0","createdAt":"2026-08-22","updatedAt":"2026-08-22","files":[{"path":"agents/openai.yaml","size":91,"sha256":"bb57e6929f0916464111ae4a5e0a2ec5d301653b5a3b818a81dc2c0a7deb21c2"}],"requires":{"mcp":[],"tools":[]},"safety":{"flags":[],"scannedAt":"2026-08-22","hasScripts":false,"networkEndpoints":["api.email.com","docs.convex.dev"]}}