Workflows as services

Expose registered workflows as stable REST and MCP endpoints using flux service — create, inspect, and manage named service definitions without restarting anything.

A Flux service is a named set of selectors that maps registered workflows to stable HTTP endpoints. Once created, a service requires no restarts when workflows are added or updated; selectors resolve dynamically against the workflow catalog at request time.

This page covers creating and managing services, calling them over HTTP, running standalone service processes, and enabling the optional MCP interface.

Prerequisites

A Flux server and at least one worker must be running. The service layer sits in front of the server and adds routing, not execution.

flux start server

# In a separate terminal:
export FLUX_WORKERS__BOOTSTRAP_TOKEN=$(flux server bootstrap-token)
flux start worker

Create a service

The simplest form binds a service name to a namespace. Every workflow in that namespace becomes an endpoint:

flux service create billing --namespace billing

To include specific workflows rather than an entire namespace:

flux service create reporting \
  --workflow billing/monthly_report \
  --workflow analytics/dashboard

To combine both — all workflows from one namespace plus specific extras, minus anything sensitive:

flux service create billing-api \
  --namespace billing \
  --workflow payments/process \
  --exclude billing/internal_cleanup

The --exclude flag takes a qualified reference (namespace/workflow_name). Exclusions apply after all includes, so a named workflow is suppressed even when an earlier include rule would have matched it.

Enable MCP at creation

Pass --mcp to expose the service’s workflows as Model Context Protocol tools in addition to REST endpoints:

flux service create billing --namespace billing --mcp

Inspect services

List all registered services:

flux service list

Show a specific service — selectors and currently resolved endpoints:

flux service show billing
Service: billing
MCP: disabled
Namespaces:
  - billing
Endpoints:
  - billing/invoice
  - billing/refund
  - billing/receipt

Both commands accept --format json for machine-readable output and --server-url to target a remote Flux server.

Modify selectors

Add and remove selectors on a live service without deleting and re-creating it.

# Add a namespace
flux service add billing --namespace payments

# Add a specific workflow
flux service add billing --workflow analytics/report

# Remove a namespace
flux service remove billing --namespace payments

# Exclude a workflow by qualified reference
flux service exclude billing billing/debug_tool

# Lift a previous exclusion
flux service include billing billing/debug_tool

add and remove modify the namespace and workflow selector lists. exclude and include modify the exclusions list. All four commands route through the same PUT /services/{name} API with different payload fields.

Toggle MCP

# Enable MCP on an existing service
flux service update billing --mcp

# Disable
flux service update billing --no-mcp

Delete a service

flux service delete billing --yes

Without --yes, the CLI prompts for confirmation.

Call a service endpoint

Any registered workflow the service selects is reachable at:

POST /services/{service}/{workflow}

The request body is the workflow’s JSON input; the response body is the raw output:

curl -X POST http://localhost:8000/services/billing/invoice \
  -H "Content-Type: application/json" \
  -d '{"customer_id": "C-100", "amount": 99.50}'

# {"invoice_id": "INV-123", "total": 99.50}

Execution modes

Append a mode segment to the URL to control how the server runs the workflow:

# Sync (default) — blocks until the workflow completes
POST /services/billing/invoice

# Async — returns immediately with an execution ID
POST /services/billing/invoice/async

# Check status of an async execution
GET  /services/billing/invoice/status/{execution_id}

# Resume a workflow that called pause()
POST /services/billing/invoice/resume/{execution_id}

Add ?detailed=true to any sync or status request to get the full execution envelope (state, namespace, workflow name) instead of raw output:

curl -X POST "http://localhost:8000/services/billing/invoice?detailed=true" \
  -H "Content-Type: application/json" \
  -d '{"customer_id": "C-100"}'
{
  "execution_id": "abc123def",
  "state": "COMPLETED",
  "output": {"invoice_id": "INV-123", "total": 99.50},
  "namespace": "billing",
  "workflow": "invoice"
}

Dynamic endpoint resolution

Selectors are evaluated at request time. Register a new workflow in the billing namespace and it is immediately available through any service that selects billing, with no update to the service definition and no restart. Delete a workflow and its endpoint returns 404.

If two selected workflows from different namespaces share a name, the service returns 409 Conflict. Resolve the collision by excluding one side with flux service exclude.

Run a standalone service process

A standalone process serves one service at root-level URLs, independent of the main Flux server. Use it for dedicated deployments where you want clean URLs and independent scaling:

flux service start billing --port 9000 --server-url http://flux-internal:8000

With the service name removed from the path, the same invoice endpoint becomes:

curl -X POST http://localhost:9000/invoice \
  -H "Content-Type: application/json" \
  -d '{"customer_id": "C-100"}'

The process forwards every request to the Flux server and caches the endpoint list with a configurable TTL (default: 60 seconds):

flux service start billing \
  --port 9000 \
  --host 0.0.0.0 \
  --server-url http://flux:8000 \
  --cache-ttl 30

A health endpoint is available automatically:

curl http://localhost:9000/health
# {"status": "healthy", "service": "billing", "endpoints": 3, "cache_age_seconds": 12.4}

MCP endpoint

When MCP is enabled, each workflow in the service gets five MCP tools:

ToolWhat it does
{name}Run the workflow synchronously
{name}_asyncRun asynchronously — returns an execution ID
resume_{name}Resume a paused execution synchronously
resume_{name}_asyncResume asynchronously
status_{name}Check execution status

If a workflow declares a Pydantic model as its input type, the generated tool exposes individual parameters matching the model’s fields, so an AI agent can call it without constructing raw JSON.

from pydantic import BaseModel
import flux

class InvoiceInput(BaseModel):
    customer_id: str
    amount: float

@flux.workflow.with_options(namespace="billing")
async def invoice(ctx: flux.ExecutionContext[InvoiceInput]):
    ...

With that definition, the invoice MCP tool exposes customer_id: str and amount: float as discrete parameters.

MCP in the standalone process

Start the standalone process with MCP enabled:

flux service start billing --port 9000 --mcp --server-url http://flux:8000

The MCP server is available at http://localhost:9000/mcp. The --mcp and --no-mcp flags on service start override the stored setting; without an explicit flag, the value stored at service creation or last update is used.

MCP authentication

The standalone MCP endpoint can validate bearer tokens from any OAuth 2.0 / OIDC identity provider and advertise the IdP for discovery (RFC 9728):

flux service start billing --port 9000 --mcp \
  --mcp-issuer https://idp.example.com/realms/my-realm \
  --mcp-audience billing-api \
  --mcp-jwks-uri https://idp.example.com/realms/my-realm/protocol/openid-connect/certs

If --mcp-issuer is omitted and your flux.toml has OIDC enabled ([flux.security.auth.oidc]), the same issuer and audience are used automatically.

What’s next