#actions and mutations

10 messages · Page 1 of 1 (latest)

trim pond
#

lets say i have a function to add a comment in my app. before adding the comment i want to sanitize the content. then save it to the db. do I need to use an action for this? this removes a lot of the nice ergonomics of mutations
also i try not to trigger actions based on your docs

slim sonnetBOT
#

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:

#

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.

trim pond
#

@slim sonnet a lot of sanitiziation requires JSDom which then requires the node environemnt

slim sonnetBOT
#

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:

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:

  1. Mutation (default runtime):

    // 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:

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.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: