#actions and mutations
10 messages · Page 1 of 1 (latest)
Hi @trim pond. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:
You don’t need an action for that; you should keep this entirely in a mutation.
Convex only requires actions when you need non‑deterministic, external side effects like HTTP requests (OpenAI, Stripe, etc.). Queries and mutations must be deterministic but can freely run pure computation on their arguments and DB data. “Sanitizing” a string (e.g. stripping HTML, trimming, escaping, profanity filtering implemented as pure code) is just deterministic computation and is fine inside a mutation. [Server functions; Mutation transactions]
So your flow can look like:
export const addComment = mutation({
args: { postId: v.id("posts"), body: v.string() },
async handler(ctx, { postId, body }) {
const sanitized = sanitize(body); // pure, deterministic
await ctx.db.insert("comments", { postId, body: sanitized });
},
});
You’d only introduce an action if the sanitization step itself required calling an external API (e.g. a 3rd‑party moderation service). In that case, the recommended pattern is:
- Mutation: capture user intent + write to DB + schedule an action.
- Action: call the external API, then call an internal mutation to write/update results. [Actions vs mutations; Actions from clients; Scheduler pattern]
This keeps your main ergonomics and invariants in mutations while using actions only where strictly necessary, in line with the best‑practices and “use actions sparingly” guidance. [Other recommendations; Zen of Convex]
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@trim pond Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@slim sonnet a lot of sanitiziation requires JSDom which then requires the node environemnt
Let me check the knowledge base for you again. One moment! :books:
You’re right about the tension here: many HTML sanitization libraries depend on Node‑only APIs (like JSDom), which pushes you toward Node actions. Here’s how that fits with Convex’s model and docs.
1. Where JSDom can run
- JSDom requires Node.js APIs, so it cannot run in the default Convex runtime.
- To use JSDom you must:
- Put the code in a
"use node"file, and - Use an action (only actions can use the Node.js runtime). [Choosing runtime; Node.js runtime]
- Put the code in a
You can’t define queries/mutations in a "use node" file, and files without "use node" can’t import ones that have it. [Node.js runtime]
2. Recommended pattern in this case
Because of that, the standard Convex pattern kicks in:
-
Mutation (default runtime):
- Capture user intent and write the raw or “pending” comment to the DB.
- Schedule an internal action to perform sanitization. [Calling actions from clients; Scheduled functions]
// convex/comments.ts (default runtime) import { v } from "convex/values"; import { mutation, internalMutation } from "./_generated/server"; import { internal } from "./_generated/api"; export const createComment = mutation({ args: { postId: v.id("posts"), body: v.string() }, handler: async (ctx, { postId, body }) => { const id = await ctx.db.insert("comments", { postId, body, status: "pending", });
await ctx.scheduler.runAfter(
0,
internal.commentSanitizer.sanitizeComment,
{ commentId: id }
);
},
});
2. **Node action**:
- Runs in `"use node"` file.
- Uses JSDom (or similar) to sanitize.
- Calls an internal mutation via `ctx.runMutation` to update the comment. [[Action context](<https://docs.convex.dev/functions/actions#action-context>); [Node.js runtime](<https://docs.convex.dev/functions/runtimes#nodejs-runtime>)]
```ts
// convex/commentSanitizer.ts
"use node";
import { action } from "./_generated/server";
import { internal } from "./_generated/api";
import { JSDOM } from "jsdom";
export const sanitizeComment = action({
args: { commentId: v.id("comments") },
handler: async (ctx, { commentId }) => {
const comment = await ctx.runQuery(
internal.comments.getById,
{ id: commentId }
);
if (!comment) return;
const dom = new JSDOM(comment.body);
const sanitizedBody = /* your sanitization with dom here */ dom.serialize();
await ctx.runMutation(internal.comments.applySanitized, {
id: commentId,
sanitizedBody,
});
},
});
// convex/comments.ts (continued, default runtime)
export const getById = internalQuery({ /* ... */ });
export const applySanitized = internalMutation({
args: { id: v.id("comments"), sanitizedBody: v.string() },
handler: async (ctx, { id, sanitizedBody }) => {
await ctx.db.patch(id, {
body: sanitizedBody,
status: "sanitized",
});
},
});
This follows the “mutation → schedule action → mutation” workflow pattern Convex recommends when an operation depends on external systems / Node‑only libraries. [Zen of Convex; Scheduler usage]
3. About “trying not to trigger actions”
The docs warn primarily against:
- Calling actions directly from clients as your main flow, because:
- They don’t integrate with optimistic updates or retries.
- They can fail or be abandoned if the client goes away. [Calling actions from clients; Discord explanation]
They explicitly recommend using mutations + scheduler + actions when external work is involved, not avoiding actions altogether. [Scheduler & actions; [Pinecone background action]
So with a JSDom‑style sanitizer, you’re in one of the “legit” action use cases:
- Keep ergonomics and invariants in a mutation.
- Use an internal Node action purely as a background, non‑deterministic step.
- Expose progress/status via queries.