You can start another workflow run inside a workflow and await its execution to complete. This allows to orchestrate multiple workflows together without external syncranization.

const {
  body,      // response from the invoked workflow
  isFailed,  // whether the invoked workflow was canceled
  isCanceled // whether the invoked workflow failed
} = await context.invoke(
  "analyze-content",
  {
    workflow: analyzeContent,
    body: "test",
    header: {...}, // headers to pass to anotherWorkflow (optional)
    retries,       // number of retries (optional, default: 3)
    flowControl,   // flow control settings (optional)
    workflowRunId  // workflowRunId to set (optional)
  }
)

As you may notice, we pass a workflow object to the invoke function. This object is initialized using the createWorkflow() function.

createWorkflow

Normally, workflows are initialized with serve() method and exposed as a standalone route in your application. However, when workflows are defined separately using serve(), type safety is not guaranteed.

To ensure type safety for request and response when invoking other workflows, we introduced the createWorkflow() function. createWorkflow() returns a referenceable workflow object that can be used in context.invoke().

import { WorkflowContext }  from "@upstash/workflow";
import { createWorkflow } from "@upstash/workflow/nextjs";

const anotherWorkflow = createWorkflow(
  // Define the workflow logic, specifying the type of the initial request body.
  // In this case, the body is a string:
  async (context: WorkflowContext<string>) => {

    await context.sleep("wait 1 second", 1)

    // Return a response from the workflow. The type of this
    // response will be available when `context.invoke` is
    // called with `anotherWorkflow`.
    return { message: "This is the data returned by the workflow" };
  }
);

const someWorkflow = createWorkflow(async (context) => {
  // Invoke anotherWorkflow with a string body and get the response
  // The types of the body parameter and the response are
  // typesafe and inferred from anotherWorkflow
  const { body } = await context.invoke(
    "invoke anotherWorkflow",
    {
      workflow: anotherWorkflow,
      body: "user-1"
    }
  ),
});

Next question is, how do we expose these workflows as endpoints? That’s where serveMany comes in.

serveMany

createWorkflow() does not expose your workflow like serve(), it just initializes the workflow object. To be able to use the workflow, they must be exposed with serveMany function.

First step of using serveMany is to define a catch-all route.

In this example, we are using Next.js. For implementations of serveMany in other frameworks, you can refer to the projects available in the examples directory of the workflow-js repository.

If you need any assistance, feel free to reach out through the chat box at the bottom right of this page.

In Next.js, a catch-all route is defined by creating a route.ts file under a directory named with [...], like app/serve-many/[...any]/route.ts

app/serve-many/[...any]/route.ts
import { WorkflowContext } from "@upstash/workflow";
import { createWorkflow, serveMany } from "@upstash/workflow/nextjs";

const workflowOne = createWorkflow(async (context) => {
  await context.run("say hi", () => {
    console.log("workflow one says hi!")
  })

  const { body, isCanceled, isFailed } = await context.invoke("invoking other", {
    workflow: workflowTwo,
    body: "hello from workflow one",
  })

  console.log(`received response from workflowTwo: ${body}`)
})

const workflowTwo = createWorkflow(async (context: WorkflowContext<string>) => {
  await context.run("say hi", () => {
    console.log("workflowTwo says hi!")
    console.log(`received: '${context.requestPayload}' in workflowTwo`)
  })

  return "Workflow two finished!"
})

export const { POST } = serveMany(
  {
    "workflow-one-route": workflowOne,
    "workflow-two-route": workflowTwo,
  }
)

If workflows are going to be invoke each other, they must be exposed in the same serveMany endpoint.

If you pass a workflow object which is initialized with createWorkflow() but not exposed inside the same serveMany, you will get a runtime error.

In this example, we have two workflows defined under serveMany. workflowOne is a workflow that invokes workflowTwo. To start workflowOne, you can send a POST request to https://your-app/serve-many/workflow-one-route.

curl -X POST https://your-app/serve-many/workflow-one-route

Note that workflow-one-route is infered from the key passed to serveMany. Similarly, you can send a POST request to https://your-app/serve-many/workflow-two-route to start workflowTwo.

Options

Just like serve, you can pass options to both createWorkflow and serveMany. createWorkflow accepts all the parameters that serve does. serveMany accepts some specific parameters.

const workflowOne = createWorkflow(
  async (context) => {
    // ...
  },
  {
    retries: 0
  }
)

export const { POST } = serveMany(
  {
    workflowOne
  },
  {
    failureUrl: "https://some-url"
  }
)

If the same parameter is provided to both createWorkflow and serveMany, the value specified in createWorkflow will take precedence.

Additionally, when you invoke workflowOne from another workflow, some options defined in createWorkflow for workflowOne will be applied in the invocation request. These options include retries, failureFunction, failureUrl, and flowControl.

Limitations

One limitation of invoke is that you cannot create an infinite chain of workflow invocations. If you set up an ‘invoke loop’ where workflows continuously invoke other workflows, the process will fail once it reaches a depth of 100.