Skip to content

Allow configuring determinism validation #35

Open

Description

@ianmacartney

Currently if you change much of anything in a workflow while there are ongoing workflows, they'll fail.

Allow configuring some things to make it more flexible, if you're willing to think through the edge cases:

  1. Allow running new steps: if you add a new step to the beginning of a workflow, existing workflows will run it. If that affects later steps, see (4, 5)
  2. Allow out of order steps: if steps change order but the args are the same, allow it. Use the first un-consumed result. If this was actually adding another earlier call to the "same" step (same fn & args), then the later one will re-run as if it's the new one.
  3. Allow removing steps: if you remove a step, carry on. If that affects later steps, see (4, 5). This also covers breaking out of a "for" loop early, e.g.
  4. Allow re-running steps if their args change (per-step opt-in): If an arg changes, re-run that step, if that step can handle retries.
  5. Allow ignoring changed args (per-step opt-in): if you add or modify args for a function, but you're fine with existing workflows using the previous result / not running it for new args.

Add good docs on all this

Activity

  1. ianmacartney commented on Apr 2, 2025

    @ianmacartney
    MemberAuthor
  2. mikecann commented on Apr 3, 2025

    @mikecann
    Member

    Ughh versioning is such a minefield and headache.

    Like if we take the example from the workflow page as an example: https://www.convex.dev/components/workflow

    import { WorkflowManager } from "@convex-dev/workflow";
    import { components } from "./_generated/api";
    
    export const workflow = new WorkflowManager(components.workflow);
    
    export const exampleWorkflow = workflow.define({
      args: {
        storageId: v.id("_storage"),
      },
      handler: async (step, args) => {
        const transcription = await step.runAction(
          internal.index.computeTranscription,
          { storageId: args.storageId },
        );
    
        // Sleep for a month after computing the transcription.
        await step.sleep(30 * 24 * 60 * 60 * 1000);
    
        const embedding = await step.runAction(internal.index.computeEmbedding, {
          transcription,
        });
        console.log(embedding);
      },
    });

    Say a user starts this and enters the sleep for a month but then you add new steps or remove others its going to totally f-up that old workflow when it resumes after a month's sleep. That might be okay if it explicitly errors as that will kill the workflow and you can handle it. The worse scenario is that it DOESNT error and instead carries, calls some new or re-arranged steps that causes it to behave in a totally unexpected way leading to corrupted data and hard to debug issues.

    I think the key issue is going to be that the steps aren't known deterministically upfront, they are only "discovered" as the workflow runs.

    This means you cant deterministically "id" a workflow definition and thus determine what (if anything) has changed. The issue is simmilar to useEffect from the react world. The only way that the handler for a useEffect can be changed is by supplying a deterministic deps array as the second param. If any of the deps change then its now a different "version" of the useEffect.

    The only way around all of theses issues is we make it mandatory that the user supplies a "version" identifier and a "versioningPolicy" when they define a workflow.

    The policy will enforce what happens to resuming steps if the version changes. For example if we have a policy "alwaysError" then when that sleep resumes after a month, if the version of the workflow is different from the one when the workflow started it errors which is probably safer than continuing on and potentially leading to confusing scenarios.

    Maybe one of the policies lets you provider a "migration" handler that allows you to do something with resuming steps if the version changes?

    Another question is what happens if that sleep step is removed entirely when going from version 1 to 2 of the workflow? Should it still re-enter the workflow?

    So I think its safer that we have an explicit version and a version policy to force the user to think about these things.

  3. ianmacartney commented on Apr 3, 2025

    @ianmacartney
    MemberAuthor

    I agree there's a problem to solve. I don't think version number is the only way to do it. We already have a known graph of what has happened so far, and things changing after what we've done so far is fine. The problem is when something changes that has already been done, which I'd say is all captured by my numbered points above. If you can think of an edge case that doesn't fall into 1-5, let me know!

    E.g. if you remove sleep, a workflow that has hit that but hasn't finished will eventually wake up and it'll be a case of (3) above, which you can opt into

    Having a migration or version policy or version number all sound compelling, but I don't think version number is necessary (even if it might be sufficient)

  4. nakleiderer commented on May 30, 2025

    @nakleiderer

    The folks over at Temporal have given this a lot of thought, and I think their patch solution is a great approach that could be applied here. It makes the behavior of migrating from one workflow to another explicit in the code instead of relying on implicit ordering or version metadata. I haven't looked at their implementation to see if it could be applied to Convex, but thought I'd mention it as a possible solution.

    https://docs.temporal.io/develop/typescript/versioning

  5. adamk-au commented on Jul 7, 2025

    @adamk-au

    While it may technically be possible to allow some edits to an existing workflow, it seems to complicate the developer's mental model of how workflows operate.

    I'm migrating from AWS Step Functions, and I prefer the immutable versioning approach. That being said, it would be good to be able to change the internals of a function. Only changes to the function signature or steps would need a new version.

    Since most of my workflows are short-lived, I'm personally ok with these tradeoffs:

    • Canceling or failing any running workflows during updates
    • Having Convex deployments fail if workflows are still running

    The version aliasing that AWS Step Functions use may be useful here too.

  6. Hades32 commented on Jul 16, 2025

    @Hades32

    we've used temporal versioning (which the TS SDK exposes as "patches") quite a bit and it works well, but does get annoying tbh. But we've also tried versioning the whole workflow (i.e. duplicating the code), and that simply doesn't cut it for long-running workflows.

    You should look into their "continue-as-new" functionality though. It helps a lot with migrating over old versions.

  7. ivryb commented on Oct 28, 2025

    @ivryb

    Is it possible to keep all the finished mutations and not roll them back if a workflow fails due to an update?
    My problem is that some steps include pretty expensive APIs, and I would like to avoid repeating them during the workflow update.

  8. ianmacartney commented on Oct 29, 2025

    @ianmacartney
    MemberAuthor
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions