# Exposing Workflows over RPC & HTTP

A durable [`Workflow`](https://effect.plants.sh/durable-workflows/) is usually invoked by importing its
implementation and calling `MyWorkflow.execute(payload)`. That only works inside
the process that holds the workflow engine. When the caller lives somewhere else
— a different service, a frontend, another team's API — you instead want a
*transport contract* the caller can speak to.

The `WorkflowProxy` and `WorkflowProxyServer` modules
(`effect/unstable/workflow`) derive that contract directly from your workflow
definitions. `WorkflowProxy` turns a list of workflows into an
[`RpcGroup`](https://effect.plants.sh/rpc/) or an [`HttpApiGroup`](https://effect.plants.sh/http-api/) that clients and servers
share; `WorkflowProxyServer` provides the server-side layer that routes the
generated operations back into the real workflows. You never hand-write a
contract, and the client and server cannot drift apart because both are
generated from the same source.

## The three derived operations

Every workflow expands into **three** operations, named from the workflow's
`name`:

| Operation              | Generated name      | Payload                  | Returns                          |
| ---------------------- | ------------------- | ------------------------ | -------------------------------- |
| Execute                | `<Name>`            | the workflow payload     | the workflow success / error     |
| Discard                | `<Name>Discard`     | the workflow payload     | `void` (fire-and-forget start)   |
| Resume                 | `<Name>Resume`      | `{ executionId: string }`| `void`                           |

- **Execute** runs the workflow and waits for its result, surfacing the
  workflow's normal `success` and `error` schemas.
- **Discard** starts the workflow on its discard path. It deliberately does *not*
  expose the success or error schema — the caller starts the run and moves on.
- **Resume** re-activates a *suspended* execution. It takes only the persisted
  `executionId`, because that boundary value identifies the in-flight run and
  cannot be recomputed from the original payload.
**Discard and resume are control operations:** Discard is fire-and-forget: callers should not rely on receiving the normal
workflow success or error value. Resume needs the `executionId` returned when
the run was first started (or surfaced by the engine when it suspended) — the
proxy cannot derive it from the workflow payload, so the client must hold onto
it.

The generated names and HTTP paths come straight from the workflow definitions.
Two rules follow from that:

- **Keep workflow `name`s stable.** Renaming a workflow renames its operations
  and breaks existing clients.
- **Pass the same workflow list (and same `prefix`) to both `WorkflowProxy` and
  the matching `WorkflowProxyServer` layer.** They must agree or the handlers
  won't line up with the contract.

## Common case

Start by defining your workflows and pinning the list with `as const` — that
keeps each workflow's literal `name`, payload, success, and error types in the
generated contract.

```ts
import { Schema } from "effect"
import { Workflow } from "effect/unstable/workflow"

const EmailWorkflow = Workflow.make({
  name: "EmailWorkflow",
  payload: {
    id: Schema.String,
    to: Schema.String
  },
  idempotencyKey: ({ id }) => id
})

// `as const` preserves the literal names/types for the derived contract
const myWorkflows = [EmailWorkflow] as const
```

Now expose them over either RPC or HTTP. Both follow the same shape: derive a
group with `WorkflowProxy`, then provide the matching server handler layer from
`WorkflowProxyServer`.

```ts
import { Layer, Schema } from "effect"
import { RpcServer } from "effect/unstable/rpc"
import { Workflow, WorkflowProxy, WorkflowProxyServer } from "effect/unstable/workflow"

const EmailWorkflow = Workflow.make({
  name: "EmailWorkflow",
  payload: { id: Schema.String, to: Schema.String },
  idempotencyKey: ({ id }) => id
})

const myWorkflows = [EmailWorkflow] as const

// Derive the shared RpcGroup from the workflows.
// Generates: EmailWorkflow, EmailWorkflowDiscard, EmailWorkflowResume
class MyRpcs extends WorkflowProxy.toRpcGroup(myWorkflows) {}

// Serve it: RpcServer.layer attaches the transport, layerRpcHandlers
// routes each RPC back into the real workflow.
const ApiLayer = RpcServer.layer(MyRpcs).pipe(
  Layer.provide(WorkflowProxyServer.layerRpcHandlers(myWorkflows))
)
```

The client uses the *same* `MyRpcs` group with `RpcClient.make` (see
[RPC: client and server](https://effect.plants.sh/rpc/client-and-server/)) and gets typed methods
`EmailWorkflow`, `EmailWorkflowDiscard`, and `EmailWorkflowResume`.

```ts
import { Layer, Schema } from "effect"
import { HttpApi, HttpApiBuilder } from "effect/unstable/httpapi"
import { Workflow, WorkflowProxy, WorkflowProxyServer } from "effect/unstable/workflow"

const EmailWorkflow = Workflow.make({
  name: "EmailWorkflow",
  payload: { id: Schema.String, to: Schema.String },
  idempotencyKey: ({ id }) => id
})

const myWorkflows = [EmailWorkflow] as const

// Derive an HttpApiGroup named "workflows" and add it to the API.
// Paths come from the lower-cased workflow name:
//   POST /emailworkflow          (execute)
//   POST /emailworkflow/discard  (discard)
//   POST /emailworkflow/resume   (resume)
class MyApi extends HttpApi.make("api").add(
  WorkflowProxy.toHttpApiGroup("workflows", myWorkflows)
) {}

// Serve it: HttpApiBuilder.layer builds the API, layerHttpApi implements
// the "workflows" group against the real workflows.
const ApiLayer = HttpApiBuilder.layer(MyApi).pipe(
  Layer.provide(
    WorkflowProxyServer.layerHttpApi(MyApi, "workflows", myWorkflows)
  )
)
```

Note the endpoint paths are the **lower-cased** workflow name, so
`EmailWorkflow` is served at `/emailworkflow`. See
[HTTP API: serving and clients](https://effect.plants.sh/http-api/serving-and-clients/) for how to mount
`ApiLayer` and generate a client.
**Note:** Both server layers require a `WorkflowEngine` plus the workflow's
`RequirementsHandler` (the services the workflow implementations need) in their
environment. Provide your workflow layers and the engine alongside `ApiLayer`.

## Choosing RPC vs HTTP

1. **Use [RPC](https://effect.plants.sh/rpc/)** when both ends are Effect services and you want the
   tightest, fully-typed client with no manual route handling. The shared
   `RpcGroup` is the only contract.

2. **Use the [HTTP API](https://effect.plants.sh/http-api/)** when callers are not Effect (browsers,
   other languages, webhooks) or you want REST-shaped, OpenAPI-documentable
   endpoints. Each workflow gets predictable `POST` routes.

Either way, remember the same three rules: stable workflow names, the same
workflow list on both sides, and a matching `prefix` (RPC) or group `name`
(HTTP) between the `WorkflowProxy` contract and the `WorkflowProxyServer` layer.

## Reference

### `WorkflowProxy.toRpcGroup`

Derives an `RpcGroup` from a non-empty list of workflows. Each workflow becomes
three RPCs: `<Prefix><Name>` (execute, with the workflow success/error),
`<Prefix><Name>Discard` (payload only), and `<Prefix><Name>Resume` (takes
`{ executionId }`). Pass `{ prefix }` to namespace the RPC tags — the same
prefix must be used by `layerRpcHandlers`.

```ts
import { Schema } from "effect"
import { Workflow, WorkflowProxy } from "effect/unstable/workflow"

const Email = Workflow.make({
  name: "Email",
  payload: { to: Schema.String },
  idempotencyKey: ({ to }) => to
})

class MyRpcs extends WorkflowProxy.toRpcGroup([Email] as const, {
  prefix: "wf."
}) {}
// => RpcGroup with RPCs: "wf.Email", "wf.EmailDiscard", "wf.EmailResume"
```

### `WorkflowProxy.toHttpApiGroup`

Derives an `HttpApiGroup` with the given group `name`. Each workflow adds three
`POST` endpoints whose paths are the lower-cased workflow name: `/<name>`
(execute), `/<name>/discard`, and `/<name>/resume`. The endpoint *names* keep
the original casing (`<Name>`, `<Name>Discard`, `<Name>Resume`).

```ts
import { Schema } from "effect"
import { HttpApi } from "effect/unstable/httpapi"
import { Workflow, WorkflowProxy } from "effect/unstable/workflow"

const Email = Workflow.make({
  name: "Email",
  payload: { to: Schema.String },
  idempotencyKey: ({ to }) => to
})

class MyApi extends HttpApi.make("api").add(
  WorkflowProxy.toHttpApiGroup("workflows", [Email] as const)
) {}
// => group "workflows" with endpoints:
//    POST /email          (name "Email")
//    POST /email/discard  (name "EmailDiscard")
//    POST /email/resume   (name "EmailResume")
```
**Tip:** There is no `prefix` option for HTTP — the group `name` is the namespace, and
endpoint paths are always derived from the lower-cased workflow name.

### `WorkflowProxyServer.layerRpcHandlers`

Builds a `Layer` of RPC handlers for the workflows, wiring each generated RPC to
the matching workflow operation: execute calls `workflow.execute(payload)`,
discard calls `workflow.execute(payload, { discard: true })`, and resume calls
`workflow.resume(executionId)`. Requires `WorkflowEngine` and the workflows'
`RequirementsHandler`. Pass the same `{ prefix }` you gave to `toRpcGroup`.

```ts
import { Layer, Schema } from "effect"
import { RpcServer } from "effect/unstable/rpc"
import { Workflow, WorkflowProxy, WorkflowProxyServer } from "effect/unstable/workflow"

const Email = Workflow.make({
  name: "Email",
  payload: { to: Schema.String },
  idempotencyKey: ({ to }) => to
})
const workflows = [Email] as const

class MyRpcs extends WorkflowProxy.toRpcGroup(workflows, { prefix: "wf." }) {}

const ApiLayer = RpcServer.layer(MyRpcs).pipe(
  // same prefix on both sides, or the handlers won't match the contract
  Layer.provide(WorkflowProxyServer.layerRpcHandlers(workflows, { prefix: "wf." }))
)
// => Layer<RpcHandlers, never, WorkflowEngine | RequirementsHandler<...>>
```

### `WorkflowProxyServer.layerHttpApi`

Builds a `Layer` implementing the HTTP API group produced by `toHttpApiGroup`,
routing execute/discard/resume endpoints to the workflows. Takes the `HttpApi`,
the group `name`, and the workflow list — all three must match what you passed
to `toHttpApiGroup`. Requires `WorkflowEngine` and the workflows'
`RequirementsHandler`.

```ts
import { Layer, Schema } from "effect"
import { HttpApi, HttpApiBuilder } from "effect/unstable/httpapi"
import { Workflow, WorkflowProxy, WorkflowProxyServer } from "effect/unstable/workflow"

const Email = Workflow.make({
  name: "Email",
  payload: { to: Schema.String },
  idempotencyKey: ({ to }) => to
})
const workflows = [Email] as const

class MyApi extends HttpApi.make("api").add(
  WorkflowProxy.toHttpApiGroup("workflows", workflows)
) {}

const ApiLayer = HttpApiBuilder.layer(MyApi).pipe(
  Layer.provide(
    // name "workflows" matches the group above
    WorkflowProxyServer.layerHttpApi(MyApi, "workflows", workflows)
  )
)
// => Layer<ApiGroup<"api", "workflows">, never, WorkflowEngine | RequirementsHandler<...>>
```

### Type-level helpers

These types describe the generated contract; you rarely reference them directly,
but they are useful when writing generic wrappers over the proxy.

#### `WorkflowProxy.ConvertRpcs<Workflows, Prefix>`

Maps each workflow to the union of its three generated RPC types
(``Rpc<`${Prefix}${Name}`, ...>``, `...Discard`, `...Resume`). This is the type
parameter of the `RpcGroup` returned by `toRpcGroup`.

```ts
import type { WorkflowProxy } from "effect/unstable/workflow"
import type { Workflow } from "effect/unstable/workflow"

type Rpcs = WorkflowProxy.ConvertRpcs<Workflow.Any, "wf.">
// => Rpc<"wf.${Name}", ...> | Rpc<"wf.${Name}Discard", ...> | Rpc<"wf.${Name}Resume", ...>
```

#### `WorkflowProxy.ConvertHttpApi<Workflows>`

Maps each workflow to the union of its three generated `HttpApiEndpoint` types,
each a `POST` at the lower-cased path. This is the contract carried by the
`HttpApiGroup` from `toHttpApiGroup`.

```ts
import type { WorkflowProxy } from "effect/unstable/workflow"
import type { Workflow } from "effect/unstable/workflow"

type Endpoints = WorkflowProxy.ConvertHttpApi<Workflow.Any>
// => POST /<name> | POST /<name>/discard | POST /<name>/resume
```

#### `WorkflowProxyServer.RpcHandlers<Workflows, Prefix>`

The union of `Rpc.Handler` services produced by `layerRpcHandlers` — the
services it provides into the environment.

```ts
import type { WorkflowProxyServer } from "effect/unstable/workflow"
import type { Workflow } from "effect/unstable/workflow"

type Handlers = WorkflowProxyServer.RpcHandlers<Workflow.Any, "">
// => Rpc.Handler<Name> | Rpc.Handler<`${Name}Discard`> | Rpc.Handler<`${Name}Resume`>
```