# DBOS Documentation
This file contains all documentation content in a single document following the llmstxt.org standard.
## AI Quickstart
You can integrate DBOS durable workflows with your AI agents (or other AI systems) to make them reliable, observable, and resilient to failures.
Rather than bolting on ad-hoc retry logic, DBOS workflows give you one consistent model for ensuring your agents can recover from any failure from exactly where they left off.
In particular, integrating DBOS to your agents gives you:
- [**Resilience to failure**](../python/tutorials/workflow-tutorial.md): Automatically recover your agents from server restarts, process crashes, network hiccups or outages, and other unexpected events.
- [**Observability and reproducibility**](./debugging.md): Monitor your agentic workflows in real time. If they exhibit unexpected behavior, use saved workflow progress to reproduce it in a development environment to identify and fix the root cause.
- [**Support for long-running agents and human-in-the-loop**](./hitl.md): Build agents that run for hours, days, or weeks (potentially waiting for human responses) and seamlessly recover from any interruption.
- [**Durable streaming**](./streaming.md): Stream output from your agents as it's generated to build interactive or conversational flows that recover from any failure.
- [**Parallel, scalable, and distributed agents**](./distributing-agents.md): Use durable queues to build agents with parallel tool calls or tasks, potentially distributing it across many servers with managed flow control.
### Get Started
You can integrate DBOS into an agent built in regular Python or TypeScript, or use native integrations with popular agentic frameworks like [Pydantic AI](https://ai.pydantic.dev/durable_execution/dbos), [LlamaIndex](https://developers.llamaindex.ai/python/llamaagents/workflows/dbos/), [OpenAI Agents SDK](https://openai.github.io/openai-agents-python/running_agents/#dbos), [Google ADK](https://adk.dev/integrations/dbos/), and the [Vercel AI SDK](https://ai-sdk.dev/).
#### 1. Install DBOS
`pip install` DBOS into your application.
```shell
pip install dbos
```
#### 2. Configure and Launch DBOS
Add these lines of code to your agent's main function.
They initialize DBOS when your agentic application starts.
```python
import os
from dbos import DBOS, DBOSConfig
config: DBOSConfig = {
"name": "my-app",
"application_version": "0.1.0",
"system_database_url": os.environ.get("DBOS_SYSTEM_DATABASE_URL"),
}
DBOS(config=config)
DBOS.launch()
```
:::info
DBOS uses a database to durably store workflow and step state.
By default, it uses SQLite, which requires no configuration.
For production use, we recommend connecting your DBOS application to a Postgres database.
When you're ready for production, you can connect this initialization code to Postgres by setting the `DBOS_SYSTEM_DATABASE_URL` environment variable to a connection string to your Postgres database.
:::
#### 3. Annotate Workflows and Steps
Next, annotate your main agentic loop as a durable workflow and each LLM and tool call it makes as a step.
This causes DBOS to checkpoint the progress of your agent in your database so it can recover from any failure.
For instance, in the [deep research agent example](../python/examples/hacker-news-agent.md), here is the main agentic loop:
```python
@DBOS.workflow()
def agentic_research_workflow(topic: str, max_iterations: int = 3):
"""
This agent starts with a research topic then:
1. Searches Hacker News for information on that topic.
2. Iteratively searches related topics, collecting information.
3. Makes decisions about when to continue.
4. Synthesizes findings into a final report.
"""
...
```
And here is an example step, an LLM call to evaluate results:
```python
@DBOS.step()
def evaluate_results_step(
topic: str,
query: str,
stories: List[Dict[str, Any]],
comments: Optional[List[Dict[str, Any]]] = None,
) -> EvaluationResult:
"""LLM evaluates search results and extracts insights."""
...
```
To learn more about how to build with DBOS Python, check out the [Python docs](../python/programming-guide.md).
#### 1. Install DBOS
`npm install` DBOS into your application.
```shell
npm install @dbos-inc/dbos-sdk@latest
```
#### 2. Configure and Launch DBOS
Add these lines of code to your agent's main function.
They initialize DBOS when your agentic application starts.
```typescript
import { DBOS } from "@dbos-inc/dbos-sdk";
DBOS.setConfig({
"name": "my-app",
"applicationVersion": "0.1.0",
"systemDatabaseUrl": process.env.DBOS_SYSTEM_DATABASE_URL,
});
await DBOS.launch();
```
:::info
DBOS uses a database to durably store workflow and step state.
By default, it uses a Postgres database.
You can start Postgres locally with `npx dbos postgres start`, or set the `DBOS_SYSTEM_DATABASE_URL` environment variable to a connection string to an existing Postgres database.
:::
#### 3. Register Workflows and Steps
Next, register your main agentic loop as a durable workflow and run each LLM and tool call as a step.
This causes DBOS to checkpoint the progress of your agent in your database so it can recover from any failure.
For instance, in the [deep research agent example](../typescript/examples/hacker-news-agent.md), here is the main agentic loop, registered as a workflow:
```typescript
async function agenticResearchWorkflowFunction(
topic: string,
maxIterations: number,
): Promise {
...
}
export const agenticResearchWorkflow = DBOS.registerWorkflow(
agenticResearchWorkflowFunction,
);
```
And here is an example step, an LLM call to evaluate results:
```typescript
const evaluation = await DBOS.runStep(
() => evaluateResults(topic, query, stories, comments),
{ name: "evaluateResults" },
);
```
To learn more about how to build with DBOS TypeScript, check out the [TypeScript docs](../typescript/programming-guide.md).
#### 1. Install Pydantic AI with DBOS
Install Pydantic AI with the DBOS optional dependency.
```shell
pip install pydantic-ai[dbos]
```
#### 2. Configure DBOS and Wrap Your Agent
Import and configure DBOS, then wrap your Pydantic AI agent in a `DBOSAgent` for durable execution.
`DBOSAgent` automatically wraps your agent's run loop as a DBOS workflow and model requests and MCP communication as DBOS steps.
```python
import asyncio
# highlight-next-line
from dbos import DBOS, DBOSConfig
from pydantic_ai import Agent
# highlight-next-line
from pydantic_ai.durable_exec.dbos import DBOSAgent
# highlight-start
dbos_config: DBOSConfig = {
'name': 'pydantic_dbos_agent',
'application_version': '0.1.0',
'system_database_url': 'sqlite:///dbostest.sqlite',
}
DBOS(config=dbos_config)
#highlight-end
agent = Agent(
'gpt-5',
instructions="You're an expert in geography.",
name='geography',
)
# highlight-next-line
dbos_agent = DBOSAgent(agent)
async def main():
# highlight-next-line
DBOS.launch()
result = await dbos_agent.run('What is the capital of Mexico?')
print(result.output)
if __name__ == "__main__":
asyncio.run(main())
```
Custom tool functions can optionally be decorated with `@DBOS.step` if they involve non-determinism or I/O.
To learn more, check out the [Pydantic AI integration guide](../integrations/pydantic-ai.md) and the [Pydantic AI docs](https://ai.pydantic.dev/durable_execution/dbos).
#### 1. Install LlamaIndex with DBOS
Install the [`llama-agents-dbos`](https://github.com/run-llama/workflows-py/tree/main/packages/llama-agents-dbos) package.
```shell
pip install llama-agents-dbos
```
#### 2. Configure DBOS and Use the DBOS Runtime
Import and configure DBOS, then create a `DBOSRuntime` and pass it to your LlamaIndex workflow.
The DBOS runtime automatically persists every workflow transition so your workflow can resume exactly where it left off after any failure.
```python
import asyncio
# highlight-next-line
from dbos import DBOS, DBOSConfig
# highlight-next-line
from llama_agents.dbos import DBOSRuntime
from pydantic import Field
from workflows import Context, Workflow, step
from workflows.events import Event, StartEvent, StopEvent
# highlight-start
config: DBOSConfig = {
"name": "llamaindex-example",
"application_version": "0.1.0",
"system_database_url": "sqlite:///example.sqlite",
}
DBOS(config=config)
# highlight-end
class MyResult(StopEvent):
output: str = Field(description="Result")
class MyWorkflow(Workflow):
@step
async def start(self, ctx: Context, ev: StartEvent) -> MyResult:
return MyResult(output="Hello from a durable workflow!")
# highlight-next-line
runtime = DBOSRuntime()
workflow = MyWorkflow(runtime=runtime)
async def main() -> None:
# highlight-next-line
await runtime.launch()
result = await workflow.run(run_id="my-run-1")
print(result.output)
asyncio.run(main())
```
To learn more, check out the [LlamaIndex integration guide](../integrations/llamaindex.md) and the [LlamaIndex docs](https://developers.llamaindex.ai/python/llamaagents/workflows/dbos/).
#### 1. Install DBOS and the OpenAI Agents Integration
Install DBOS and the [durable OpenAI agents integration](https://github.com/dbos-inc/dbos-openai-agents).
```shell
pip install dbos dbos-openai-agents
```
#### 2. Configure DBOS and Wrap Your Agent
Use `DBOSRunner` as a drop-in replacement for `Runner` and annotate your agent's workflow and tool calls with DBOS decorators.
```python
import asyncio
from agents import Agent, function_tool
# highlight-start
from dbos import DBOS, DBOSConfig
from dbos_openai_agents import DBOSRunner
#highlight-end
@function_tool
# highlight-next-line
@DBOS.step()
async def get_weather(city: str) -> str:
"""Get the weather for a city."""
return f"Sunny in {city}"
agent = Agent(name="weather", tools=[get_weather])
# highlight-start
@DBOS.workflow()
async def run_agent(user_input: str) -> str:
result = await DBOSRunner.run(agent, user_input)
return str(result.final_output)
# highlight-end
async def main():
# highlight-start
config: DBOSConfig = {
"name": "my-agent",
"application_version": "0.1.0",
"system_database_url": 'sqlite:///my_agent.sqlite',
}
DBOS(config=config)
DBOS.launch()
# highlight-end
output = await run_agent("How is the weather in San Francisco")
print(output)
if __name__ == "__main__":
asyncio.run(main())
```
To learn more, check out the [OpenAI Agents SDK integration guide](../integrations/openai-agents.md) and the [OpenAI Agents SDK documentation](https://openai.github.io/openai-agents-python/running_agents/#dbos).
#### 1. Install DBOS and the Google ADK Integration
Install DBOS and the [durable Google ADK agents integration](https://github.com/dbos-inc/dbos-google-adk).
```shell
pip install dbos dbos-google-adk
```
#### 2. Configure DBOS and Wrap Your Agent
Add `DBOSPlugin` to your `Runner` and annotate your agent's workflow and tool calls with DBOS decorators.
```python
import asyncio
import logging
# highlight-start
from dbos import DBOS, DBOSConfig
from dbos_google_adk import DBOSPlugin
# highlight-end
from google.adk.agents import LlmAgent
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai import types
# Decorate tool calls with @DBOS.step() for durable execution
# highlight-next-line
@DBOS.step()
async def get_weather(city: str) -> str:
"""Get the weather for a city."""
return f"Sunny in {city}"
agent = LlmAgent(name="weather", model="gemini-flash-latest", tools=[get_weather])
runner = Runner(
app_name="my-agent",
agent=agent,
# highlight-next-line
plugins=[DBOSPlugin()],
session_service=InMemorySessionService(),
)
# Drive the agent from a DBOS workflow for durable execution
# highlight-next-line
@DBOS.workflow()
async def run_agent(user_id: str, session_id: str, message: str) -> str:
new_message = types.Content(role="user", parts=[types.Part.from_text(text=message)])
async for event in runner.run_async(
user_id=user_id, session_id=session_id, new_message=new_message
):
if event.is_final_response():
return event.content.parts[0].text
return ""
async def main():
# highlight-start
# DBOS checkpoints to SQLite by default. Postgres is recommended for production.
config: DBOSConfig = {"name": "my-agent", "application_version": "0.1.0", "system_database_url": "sqlite:///dbostest.sqlite"}
DBOS(config=config)
DBOS.launch()
# highlight-end
await runner.session_service.create_session(
app_name="my-agent", user_id="u", session_id="s"
)
print(await run_agent("u", "s", "How is the weather in San Francisco?"))
if __name__ == "__main__":
asyncio.run(main())
```
To learn more, check out the [Google ADK integration guide](../integrations/google-adk.md) and the [Google ADK documentation](https://adk.dev/integrations/dbos).
#### 1. Install DBOS and the Vercel AI Integration
Install DBOS and the [Vercel AI SDK integration](https://www.npmjs.com/package/@dbos-inc/vercel-ai).
```shell
npm install @dbos-inc/vercel-ai @dbos-inc/dbos-sdk ai
```
#### 2. Wrap Your Model and Run Your Agent in a Workflow
Wrap your model with `durableCalls` middleware so every model call is checkpointed, then register your agent's generation loop as a DBOS workflow.
DBOS checkpoints the progress of your agent in your database so it can recover from any failure, replaying completed model calls from their checkpoints instead of re-contacting the provider.
```typescript
// highlight-start
import { DBOS } from '@dbos-inc/dbos-sdk';
import { durableCalls } from '@dbos-inc/vercel-ai';
// highlight-end
import { generateText, wrapLanguageModel } from 'ai';
import { openai } from '@ai-sdk/openai';
// Wrap your model so every model call is checkpointed in Postgres
// highlight-next-line
const model = wrapLanguageModel({
model: openai('gpt-5'),
// highlight-next-line
middleware: durableCalls({ retriesAllowed: true, maxAttempts: 5 }),
});
// Register your agent's generation loop as a durable workflow
// highlight-next-line
const researchAgent = DBOS.registerWorkflow(
async (question: string) => {
const { text } = await generateText({
model,
prompt: question,
system: 'You are a helpful research assistant.',
});
return text;
},
{ name: 'researchAgent' },
);
async function main() {
// highlight-start
DBOS.setConfig({ name: 'my-agent', systemDatabaseUrl: process.env.DBOS_SYSTEM_DATABASE_URL });
await DBOS.launch();
// highlight-end
console.log(await researchAgent('Why did the agent cross the road?'));
}
main();
```
Wrap your tools with [`durableTools`](../integrations/vercel-ai.md#durable-tools) to make their side effects durable too. The integration also supports [durable streams](../integrations/vercel-ai.md#durable-streams), [MCP tools](../integrations/vercel-ai.md#durable-mcp-tools), [subagents](../integrations/vercel-ai.md#durable-subagents), [embeddings](../integrations/vercel-ai.md#durable-embedding-models), and [image generation](../integrations/vercel-ai.md#durable-image-models).
To learn more, check out the [Vercel AI SDK integration guide](../integrations/vercel-ai.md) and the [Vercel AI SDK documentation](https://ai-sdk.dev/).
### Using Coding Agents
DBOS provides skills and prompts to help you use coding agents to add DBOS to your AI applications.
Learn more about them here:
- [AI-assisted development in Python](../python/prompting.md)
- [AI-assisted development in TypeScript](../typescript/prompting.md)
- [AI-assisted development in Go](../golang/prompting.md)
- [AI-assisted development in Java](../java/prompting.md)
Additionally, DBOS provides an MCP server so your agents can observe and monitor your workflows and help you find and catch issues.
Learn more about it [here](../integrations/mcp.md).
---
## Observability & Reproducibility
One of the most common problems you encounter building and operating agents is **debugging failures**, particularly those caused by unexpected agent behavior.
For example, an agent might:
- Return a malformed structured output, causing a tool call to fail.
- Invoke the wrong tool or the right tool with the wrong inputs, causing the tool to fail.
- Generate an undesirable or inappropriate text output, with potentially business-critical consequences.
These behaviors are especially hard to diagnose in a complex or long-running agent—if an agent runs for two hours then fails unexpectedly, it's difficult to reproduce the exact set of conditions that caused the failure and test a fix.
Durable workflows help by making it easier to **observe** the root cause of the failure, deterministically **reproduce** the failure, and **test or apply** fixes.
Because workflows checkpoint the outcome of each step of your workflow, you can review these checkpoints to see the cause of the failure and audit every step that led to it.
For example, using the [DBOS Console dashboard](../conductor/workflow-management.md), you might see that your agent failed because of a validation error caused by a malformed structured output:
Once you've identified the cause of a failure, you can use the [**workflow fork**](../python/tutorials/workflow-management.md#forking-workflows) operation to reproduce it.
Fork restarts a workflow from a completed step, using checkpointed information to deterministically reproduce the state of the workflow up to that step.
Thus, you can rerun the misbehaving step under the exact conditions that originally caused the misbehavior.
Once you can reproduce a failure in a development environment, it becomes much easier to fix.
You can add additional logging or telemetry to the misbehaving step to identify the root cause.
Then, when you have a fix, you can reproduce the failure with the fix in place to test if it works.
For example, if you hypothesize that the malformed output was caused by an error in the prompt, you can fix the prompt, rerun the failed step, and watch it complete successfully:
---
## Parallelizing & Scaling Agents
AI agents and applications often need to **run many tasks in parallel**.
A single step of an agentic loop might invoke several tools at once based on an LLM response.
A document ingestion pipeline using Retrieval-Augmented Generation (RAG) might index tens of thousands of documents concurrently.
A deep research agent might scrape hundreds of websites at the same time.
DBOS workflows make these parallel patterns durable and scalable through a **durable queue** abstraction.
A workflow can enqueue any number of tasks for concurrent processing, then wait for their results.
Because every task is checkpointed, your agent can recover from any failure mid-flight without re-running work that already succeeded.
### Parallel Tool Calls
When an LLM returns multiple tool calls in a single response, you can execute them in parallel by enqueuing each one as a workflow and waiting for all of them to complete:
```python
DBOS.register_queue("tool_queue")
@DBOS.workflow()
def run_tool_calls(tool_calls):
handles: List[WorkflowHandle] = []
# Enqueue each tool call to run in parallel
for call in tool_calls:
handle = DBOS.enqueue_workflow("tool_queue", execute_tool, call.name, call.arguments)
handles.append(handle)
# Wait for all tool calls to finish and collect their outputs
return [handle.get_result() for handle in handles]
```
Because each tool call runs as a durable workflow, an agent that crashes partway through a fan-out resumes without re-issuing tools that already completed—an important property when tools have side effects or call expensive APIs.
### Distributing Work Across Servers
The same pattern scales to data pipelines and other batch workloads.
For example, a document ingestion pipeline can enqueue a workflow to index each document in a batch:
```python
DBOS.register_queue("indexing_queue")
@DBOS.workflow()
def index_documents(urls):
handles: List[WorkflowHandle] = []
# Enqueue each document for indexing
for url in urls:
handle = DBOS.enqueue_workflow("indexing_queue", index_document, url)
handles.append(handle)
# Wait for all documents to finish indexing, count the total number of indexed pages
outputs = []
for handle in handles:
outputs.append(handle.get_result())
return outputs
```
Enqueued workflows can be dequeued and executed by any of your application's servers, distributing the work across your fleet.
If your application is resource intensive or uses rate-limited APIs, you can use queues to rate-limit or control the concurrency of your workflows.
For example, you can specify that no more than 10 workflows should run concurrently on a single server:
```python
DBOS.register_queue("indexing_queue", worker_concurrency=10)
```
Because queues are backed by durable workflows, they automatically recover from any failure: if a server restarts or has a network hiccup partway through a multi-hour run of your pipeline on a batch of 10K documents, your pipeline will recover from the last indexed document instead of restarting from the beginning and redoing expensive work.
If you're interested in building distributed AI agents or data pipelines, check out the [document ingestion example](../python/examples/document-detective.md), which shows best practices for building durable distributed applications.
To learn more about how to scale applications with durable queues, check out the [queues tutorial](../python/tutorials/queue-tutorial.md).
---
## Reliable Human-in-the-Loop
Many agents need a **human in the loop** for decisions that are too important to fully trust an LLM.
However, it's not easy to design an agent that waits for human feedback.
The key issue is **time**: a human might take hours or days to respond to an agent, so the agent must be able to reliably wait for a long time (during which the server might be restarted, software might be upgraded, etc.).
Durable workflows help because they provide tools like **durable messaging** and **workflow events** that let agents durably communicate with the outside world.
### Waiting for Human Input
You can use [`DBOS.recv`](../python/tutorials/workflow-communication.md#recv) inside a workflow to durably wait for a message.
For example, you can add a line of code to your agent that tells it to wait hours or days for a notification:
```python
approval: Optional[HumanResponseRequest] = DBOS.recv(timeout_seconds=TIMEOUT)
```
Because the workflow's progress is checkpointed and both the deadline and notification are stored in your database, this can safely wait for a long time.
Anything can happen while your agent is waiting (its server can restart, its code can be upgraded, etc.) and it will recover and keep waiting until the notification arrives or the deadline is reached.
To send that notification (for example, from an HTTP endpoint), use [`DBOS.send`](../python/tutorials/workflow-communication.md#send):
```python
@app.post("/agents/{agent_id}/respond")
def respond_to_agent(agent_id: str, response: HumanResponseRequest):
DBOS.send(agent_id, response)
return {"ok": True}
```
All messages are persisted to the database, so if `send` completes successfully, the destination workflow is guaranteed to receive it.
### Publishing Agent Status
Agents can publish their current status using [`DBOS.set_event`](../python/tutorials/workflow-communication.md#workflow-events), and external code can read it with [`DBOS.get_event`](../python/tutorials/workflow-communication.md#get_event).
This is useful for letting a frontend know what an agent is doing, whether it's working, waiting for approval, or finished:
```python
@DBOS.workflow()
def durable_agent(request: AgentStartRequest):
agent_status = AgentStatus(status="working", ...)
DBOS.set_event(AGENT_STATUS, agent_status)
# Do some work, then request approval
agent_status.status = "pending_approval"
DBOS.set_event(AGENT_STATUS, agent_status)
approval = DBOS.recv(timeout_seconds=TIMEOUT)
if approval is not None and approval.response == "approve":
agent_status.status = "working"
DBOS.set_event(AGENT_STATUS, agent_status)
# Continue execution...
else:
agent_status.status = "denied"
DBOS.set_event(AGENT_STATUS, agent_status)
raise Exception("Agent denied or timed out")
```
You can then use the [workflow introspection API](../python/reference/contexts.md#list_workflows) and [`DBOS.get_event`](../python/tutorials/workflow-communication.md#get_event) to monitor and display your agents, for example to build an "inbox" of all agents currently waiting for human input:
```python
@app.get("/agents/waiting")
async def list_waiting_agents():
agent_workflows = await DBOS.list_workflows_async(
status="PENDING", name=durable_agent.__qualname__
)
statuses = await asyncio.gather(
*[DBOS.get_event_async(w.workflow_id, AGENT_STATUS) for w in agent_workflows]
)
return [s for s in statuses if s.status == "pending_approval"]
```
For a complete working example, check out the [agent inbox application](../python/examples/agent-inbox.md).
To learn more about `send`/`recv`, `set_event`/`get_event`, and streaming, see the [workflow communication docs](../python/tutorials/workflow-communication.md).
---
## Streaming Responses
AI agents often need to **stream output to clients in real time**, for example, to display LLM output as it is generated, surface intermediate tool results, or report the progress of a long-running task.
DBOS workflows provide **durable streams**: append-only channels you can write to from inside a workflow and read from anywhere in your application.
Every write is persisted, so if a server restarts mid-response the workflow recovers from where it left off and the reader keeps receiving values without dropping output.
### Writing to a Stream
Inside a workflow or step, write values to a stream identified by a string key.
When you're done producing values, close the stream so readers know they've received everything; otherwise streams are automatically closed when the workflow terminates.
**Python**
This example streams an LLM response as it's generated:
```python
from openai import OpenAI
client = OpenAI()
@DBOS.step()
def stream_completion(prompt: str, stream_key: str) -> str:
full_response = ""
response = client.chat.completions.create(
model="gpt-5",
messages=[{"role": "user", "content": prompt}],
stream=True,
)
for chunk in response:
token = chunk.choices[0].delta.content
if token:
DBOS.write_stream(stream_key, token)
full_response += token
return full_response
@DBOS.workflow()
def chat_workflow(prompt: str) -> str:
answer = stream_completion(prompt, "tokens")
DBOS.close_stream("tokens")
return answer
```
**TypeScript**
This example streams an LLM response as it's generated:
```typescript
import OpenAI from "openai";
const client = new OpenAI();
async function streamCompletion(
prompt: string,
streamKey: string,
): Promise {
let fullResponse = "";
const response = await client.chat.completions.create({
model: "gpt-5",
messages: [{ role: "user", content: prompt }],
stream: true,
});
for await (const chunk of response) {
const token = chunk.choices[0].delta.content;
if (token) {
await DBOS.writeStream(streamKey, token);
fullResponse += token;
}
}
return fullResponse;
}
async function chatWorkflowFunction(prompt: string): Promise {
const answer = await DBOS.runStep(
() => streamCompletion(prompt, "tokens"),
{ name: "streamCompletion" },
);
await DBOS.closeStream("tokens");
return answer;
}
export const chatWorkflow = DBOS.registerWorkflow(chatWorkflowFunction);
```
### Reading from a Stream
You can read from a stream using its workflow ID and key from anywhere in your application.
The reader yields values in order until the stream is closed or the workflow terminates.
For example, start an agentic workflow and print its output as it's written:
**Python**
```python
handle = DBOS.start_workflow(chat_workflow, "Tell me a joke")
for token in DBOS.read_stream(handle.workflow_id, "tokens"):
print(token, end="", flush=True)
```
**TypeScript**
```typescript
const handle = await DBOS.startWorkflow(chatWorkflow)("Tell me a joke");
for await (const token of DBOS.readStream(handle.workflowID, "tokens")) {
process.stdout.write(token);
}
```
You can also read streams from outside your application using a [DBOS Client](../python/reference/client.md#read_stream).
To learn more, see the workflow streaming tutorial ([Python](../python/tutorials/workflow-communication.md#workflow-streaming), [TypeScript](../typescript/tutorials/workflow-communication.md#workflow-streaming)).
---
## DBOS Architecture
DBOS provides a high-performance, easy-to-use library for durable workflows built on top of Postgres.
You use DBOS by installing the open-source library into your application and annotating workflows and steps.
While your application runs, DBOS checkpoints those workflows and steps to a Postgres database.
When failures occur, whether from crashes, interruptions, or restarts, DBOS uses those checkpoints to recover each of your workflows from the last completed step.
Architecturally, an application built with DBOS looks like the below diagram.
The open-source DBOS library uses Postgres to orchestrate durable workflows and queues.
There's no separate orchestration server and no infrastructure required besides Postgres.
When running in production, we also recommend connecting your DBOS applications to [Conductor](#operating-dbos-in-production-with-conductor), a "control plane" for your durable workflows that coordinates workflow recovery to guarantee high availability and provides operational tooling such as an admin UI and dashboard, observability integrations, and managed workflow retention policies.
To learn more about how to add DBOS to your application, check out the language-specific integration guides ([Python](./python/integrating-dbos.md), [TypeScript](./typescript/integrating-dbos.md), [Go](./golang/integrating-dbos.md), [Java](./java/integrating-dbos.md)).
### Using DBOS in a Distributed Setting
You can create a distributed DBOS application by launching multiple server processes (sometimes called "workers" or "executors") on a variety of platforms, such as a Kubernetes cluster, a fleet of EC2 instances, or a serverless platform like Google Cloud Run.
Within an application, each server must connect to the same Postgres database, called the system database.
This database stores all workflow checkpoints, step outputs, and schedule and queue state.
To distribute work across many servers in a cluster, you should use [durable queues](#durable-queues).
Distributed applications should also connect to [DBOS Conductor](#operating-dbos-in-production-with-conductor), the control plane for cluster-wide observability and management.
For example, if one of your workers crashes or fails, Conductor detects the failure and automatically recovers its workflows to a compatible live worker.
When using DBOS in a distributed setting, you often want to implement durable workflows in one service, but manage them from another service.
For example, you may want your API server to enqueue and monitor durable jobs on your data processing service.
You can use the DBOS Client ([Python](./python/reference/client.md), [TypeScript](./typescript/reference/client.md), [Go](./golang/reference/dbos-context.md#newclient), [Java](./java/reference/client.md)) to programmatically interact with your application from external code.
Your API server can create a client connected to your data processing service's system database and use it to enqueue a job, monitor the job's status, and retrieve its result when complete.
Here's a diagram of what that might look like:
You may also have multiple applications or services that need durable workflows.
For example, you might have a service that runs business workflows, a service that handles data ingestion, and a service that runs an AI agent.
You can separately add DBOS to each of these applications.
Each application must have a unique name.
You can give each application its own system database (this doesn't require multiple Postgres servers: a single physical Postgres server can host multiple logical system databases), or multiple applications can [share a single system database](./explanations/sharing-a-system-database.md).
Applications sharing a system database are isolated from one another, each running only its own workflows, queues, and schedules, but can interoperate: for example, one application can enqueue another's workflows and wait for their results.
Within an application, all servers must use the same programming language. However, cross-language interaction is possible via the DBOS Client, or by sharing a system database between applications in different languages. For example, a TypeScript application can enqueue workflows onto a separate Python application, monitor their progress, and gather results.
Cross-language operations are documented [here](./explanations/portable-workflows.md).
### How DBOS Scales
You can easily scale a DBOS application by adding more servers to it, so the scalability of DBOS is fundamentally determined by the database it is connected to.
The only overhead DBOS adds is database writes: one database write per step (to checkpoint the step's outcome) plus two additional database writes per workflow (one at the beginning to checkpoint workflow inputs, one at the end to checkpoint the workflow outcome).
In [benchmarks](https://www.dbos.dev/blog/benchmarking-workflow-execution-scalability-on-postgres), a DBOS application using a single Postgres database can sustain a throughput of >40K workflows or steps per second.
Scaling beyond that is possible by sharding workflows across multiple Postgres databases.
It is worth noting that since DBOS checkpoints workflow inputs and outputs and step outputs, the sizes of its writes are determined by the sizes of your inputs and outputs.
If your steps return small objects, the write sizes are negligible, but if they return large files, the write sizes are large.
Thus, we recommend architecting steps to avoid large output sizes (for example, store large files in cloud blob storage like S3 and have steps return pointers to those files).
### How Workflow Recovery Works
DBOS achieves fault tolerance by checkpointing workflows and steps.
Every workflow input and step output is durably stored in the system database.
When workflow execution fails, whether from crashes, network issues, or server restarts, DBOS leverages these checkpoints to recover workflows from their last completed step.
Workflow recovery occurs in three steps:
1. First, DBOS detects interrupted workflows.
In single-node deployments, this happens automatically at startup when DBOS scans for incomplete (PENDING) workflows.
In a distributed deployment, some coordination is required, either automatically through services like [DBOS Conductor](#operating-dbos-in-production-with-conductor) or [manually](./production/workflow-recovery.md).
2. Next, DBOS restarts each interrupted workflow by calling it with its checkpointed inputs.
As the workflow re-executes, it checks before each step if that step's output is checkpointed in Postgres.
If there is a checkpoint, the step returns the checkpointed output instead of executing.
3. Eventually, the recovered workflow reaches a step with **no checkpoint**.
This marks the point where the original execution failed.
The recovered workflow executes that step normally and proceeds from there, thus **resuming from the last completed step.**
For DBOS to be able to safely recover a workflow, your code must satisfy two requirements:
1. The workflow function must be **deterministic**: if executed multiple times, with the same arguments and step return values, the workflow should invoke the same steps with the same inputs in the same order. If you need to perform any non-deterministic operation like accessing the database, calling a third-party API, generating a random number, or getting the local time, you should do it in a step instead of directly in the workflow function.
2. Steps should be **idempotent**, meaning it should be safe to retry them multiple times.
If a workflow fails while executing a step, it retries the step during recovery.
However, once a step completes and is checkpointed, it is never re-executed.
### Upgrading Workflow Code
One challenge you may encounter when operating long-running durable workflows in production is **how to deploy breaking changes without disrupting in-progress workflows.**
A breaking change to a workflow is any change in what steps run or the order in which steps run.
The issue is that if a breaking change was made to a workflow, the checkpoints created by a workflow that started on the previous version of the code may not match the steps called by the workflow in the new version of the code, which makes the workflow difficult to recover.
DBOS supports two strategies for safely upgrading workflow code: **patching** and **versioning**.
When using patching, you add DBOS patch statements to your code to make a breaking change in a conditional so old workflows can safely recover.
When using versioning, DBOS versions applications and workflows so workflows only recover to processes running compatible code.
Learn more about both strategies in the workflow upgrade tutorial ([Python](./python/tutorials/upgrading-workflows.md), [TypeScript](./typescript/tutorials/upgrading-workflows.md), [Go](./golang/tutorials/upgrading-workflows.md), [Java](./java/tutorials/upgrading-workflows.md)).
### Durable Queues
One powerful feature of DBOS is that you can **enqueue** workflows for distributed execution with flow control.
You can enqueue a workflow from within your DBOS application using the DBOS library or from another application using a DBOS Client ([Python](./python/reference/client.md), [TypeScript](./typescript/reference/client.md), [Go](./golang/reference/dbos-context.md#newclient), [Java](./java/reference/client.md)).
An enqueued workflow may be dequeued and executed by your application's servers.
All processes running DBOS periodically poll queues to find and execute new work.
You can configure which processes listen to which queues.
To help you operate at scale, DBOS queues provide **flow control**.
You can customize the rate and concurrency at which workflows are dequeued and executed.
For example, you can set a **worker concurrency** for each of your queues on each of your servers, limiting how many workflows from that queue may execute concurrently on that server.
For more information on queues, see the docs ([Python](./python/tutorials/queue-tutorial.md), [TypeScript](./typescript/tutorials/queue-tutorial.md), [Go](./golang/tutorials/queue-tutorial.md), [Java](./java/tutorials/queue-tutorial.md)).
### Operating DBOS in Production with Conductor
When operating DBOS durable workflows in production, we strongly recommend connecting your application to Conductor.
Conductor is the control plane for your durable workflows, providing:
- [**High availability**](./production/workflow-recovery.md): In a distributed environment with many executors running durable workflows, Conductor automatically detects when the execution of a durable workflow is interrupted (for example, if its executor is restarted, interrupted, or crashes) and recovers the workflow to another healthy executor.
- [**Workflow and queue observability**](./conductor/workflow-management.md): Conductor provides dashboards of all active and past workflows and all queued tasks as well as real-time workflow visualization.
- [**Workflow and queue management**](./conductor/workflow-management.md): From the Conductor dashboard, you can pause any workflow execution, start any stopped or enqueued workflow, or restart any workflow from a specific step. This is useful for rapidly responding to incidents or debugging.
- [**Managed Retention Policies**](./conductor/retention.md): From the Conductor dashboard, manage how much workflow history each of your applications should retain and for how long to retain it.
- [**Autoscaling and version management**](./conductor/autoscaling.md): Conductor computes how many executors each version of your application needs from queue utilization, so autoscalers like KEDA can size a deployment per application version, drain old versions down to zero, and drive rollouts.
- [**Observability Integrations**](./conductor/metrics.md): Conductor exposes metrics about your applications' workflows, steps, and executors from a Prometheus-compatible endpoint, so you can monitor your DBOS applications in Datadog, Grafana, or any other tool that understands the OpenMetrics format.
Architecturally, Conductor looks like this:
Each of your application servers opens a secure websocket connection to Conductor.
All of Conductor's capabilities are powered by these websocket connections.
When you open a Conductor dashboard in your browser, your request is sent over websocket to one of your application servers, which serves the request (for example, retrieving a list of recent workflows) and sends the result back through the websocket.
If one of your application servers fails, Conductor detects the failure through the closed websocket connection and, after a grace period, directs another server to recover its workflows.
This architecture has two useful implications:
1. Conductor is **secure** and **privacy-preserving**. It does not have access to your database, nor does it need direct access to your application servers. Instead, your servers open outbound websocket connections to it and communicate exclusively through its websocket protocol.
2. Conductor is **off your workflows orchestration path**. Conductor drives observability, recovery, and retention policies, and is never involved in workflow execution (unlike the external orchestrators of other workflow systems).
If your application's connection to Conductor is interrupted, it will continue to operate normally, and any failed workflows will automatically be recovered as soon as the connection is restored.
For more information on Conductor, see [its docs](./conductor/overview.md).
---
## Alerting
If you are using [Conductor](./overview.md), you can configure automatic alerts when certain failure conditions are met.
You can configure alerts either in Conductor directly or on [Conductor-exported metrics](#metrics-based-alerts) using your existing observability stack.
:::info
Alerts require at least a [DBOS Teams](https://www.dbos.dev/dbos-pricing) plan.
:::
#### Creating Alerts
You can create new alerts (or view or update your existing alerts) from your application's "Alerting" page on the DBOS Console.
Currently, you can create alerts for the following failure conditions:
- If a certain number of workflows (parameterizable by workflow type) fail in a set period of time.
- If a workflow remains enqueued for more than a certain period of time (parameterizable by queue name), indicating the queue is overwhelmed or stuck.
- If an application is unresponsive (no connected executors, or connected but unresponsive executors).
If multiple applications [share a system database](../explanations/sharing-a-system-database.md), each application's alerts consider only the workflows it owns.
You may also specify an application to receive the alert—this does not need to be the same as the application that generated the alert.
For some failure conditions (e.g., unresponsive application), the application receiving the alert is required to be different from the one generating it.
#### Receiving Alerts
You can register an alert handler in your application to receive alerts from Conductor.
Your handler can log the alerts or forward them to another system, such as Slack or PagerDuty.
Only one alert handler may be registered per application, and it must be registered before launching DBOS.
If no handler is registered, alerts are logged automatically.
The handler receives three arguments:
- **rule_type**: The type of alert rule. One of `WorkflowFailure`, `SlowQueue`, or `UnresponsiveApplication`.
- **message**: The alert message.
- **metadata**: Additional key-value string metadata about the alert. The metadata keys depend on the rule type:
**`WorkflowFailure`**:
- `workflow_name`: The workflow name filter, or `*` for all workflows.
- `failed_workflow_count`: The number of failed workflows detected in the time window.
- `threshold`: The configured failure count threshold.
- `period_secs`: The time window in seconds.
**`SlowQueue`**:
- `queue_name`: The queue name filter, or `*` for all queues.
- `stuck_workflow_count`: The number of workflows that have been in the queue for longer than the time threshold.
- `threshold_secs`: The enqueue time threshold in seconds.
**`UnresponsiveApplication`**:
- `application_name`: The application name.
- `connected_executor_count`: The number of executors currently connected to this application.
**Python**
Example logging alerts:
```python
from dbos import DBOS
@DBOS.alert_handler
def handle_alert(rule_type: str, message: str, metadata: dict[str, str]) -> None:
DBOS.logger.warning(f"Alert received: {rule_type} - {message}")
for key, value in metadata.items():
DBOS.logger.warning(f" {key}: {value}")
```
Example forwarding alerts to Slack using [incoming webhooks](https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks/)
```python
@DBOS.alert_handler
def handle_alert(rule_type: str, message: str, metadata: dict[str, str]) -> None:
webhook_url = os.environ.get("SLACK_WEBHOOK_URL")
slack_text = f"*Alert: {rule_type}*\n{message}\n" + "\n".join(f"• {k}: {v}" for k, v in metadata.items())
try:
resp = requests.post(webhook_url, json={"text": slack_text}, timeout=10)
resp.raise_for_status()
except requests.RequestException as e:
DBOS.logger.error(f"Failed to send Slack alert: {e}")
```
Example forwarding alerts to PagerDuty using the [Events API](https://developer.pagerduty.com/docs/events-api-v2-overview):
```python
@DBOS.alert_handler
def handle_alert(rule_type: str, message: str, metadata: dict[str, str]) -> None:
routing_key = os.environ.get("PAGERDUTY_ROUTING_KEY")
payload = {
"routing_key": routing_key,
"event_action": "trigger",
"payload": {
"summary": f"{rule_type}: {message}",
"severity": "error",
"source": "my-app",
"custom_details": metadata,
},
}
try:
resp = requests.post(
"https://events.pagerduty.com/v2/enqueue",
json=payload,
timeout=10,
)
resp.raise_for_status()
except requests.RequestException as e:
DBOS.logger.error(f"Failed to send PagerDuty alert: {e}")
```
See the [Python reference](../python/reference/contexts.md#alert_handler) for more details.
**Go**
Example logging alerts:
```go
dbos.SetAlertHandler(dbosContext, func(ruleType string, message string, metadata map[string]string) {
slog.Warn(fmt.Sprintf("Alert received: %s - %s", ruleType, message))
for key, value := range metadata {
slog.Warn(fmt.Sprintf(" %s: %s", key, value))
}
})
```
Example forwarding alerts to Slack using [incoming webhooks](https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks/)
```go
dbos.SetAlertHandler(dbosContext, func(ruleType string, message string, metadata map[string]string) {
webhookURL := os.Getenv("SLACK_WEBHOOK_URL")
var metaParts []string
for k, v := range metadata {
metaParts = append(metaParts, fmt.Sprintf("• %s: %s", k, v))
}
slackText := fmt.Sprintf("*Alert: %s*\n%s\n%s", ruleType, message, strings.Join(metaParts, "\n"))
body, _ := json.Marshal(map[string]string{"text": slackText})
resp, err := http.Post(webhookURL, "application/json", bytes.NewReader(body))
if err != nil {
slog.Error(fmt.Sprintf("Failed to send Slack alert: %v", err))
return
}
defer resp.Body.Close()
})
```
Example forwarding alerts to PagerDuty using the [Events API](https://developer.pagerduty.com/docs/events-api-v2-overview):
```go
dbos.SetAlertHandler(dbosContext, func(ruleType string, message string, metadata map[string]string) {
routingKey := os.Getenv("PAGERDUTY_ROUTING_KEY")
payload := map[string]any{
"routing_key": routingKey,
"event_action": "trigger",
"payload": map[string]any{
"summary": fmt.Sprintf("%s: %s", ruleType, message),
"severity": "error",
"source": "my-app",
"custom_details": metadata,
},
}
body, _ := json.Marshal(payload)
resp, err := http.Post("https://events.pagerduty.com/v2/enqueue", "application/json", bytes.NewReader(body))
if err != nil {
slog.Error(fmt.Sprintf("Failed to send PagerDuty alert: %v", err))
return
}
defer resp.Body.Close()
})
```
See the [Go reference](../golang/reference/methods.md#alerting) for more details.
**Typescript**
Example logging alerts:
```typescript
DBOS.setAlertHandler(async (ruleType: string, message: string, metadata: Record) => {
DBOS.logger.warn(`Alert received: ${ruleType} - ${message}`);
for (const [key, value] of Object.entries(metadata)) {
DBOS.logger.warn(` ${key}: ${value}`);
}
});
```
Example forwarding alerts to Slack using [incoming webhooks](https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks/)
```typescript
DBOS.setAlertHandler(async (ruleType: string, message: string, metadata: Record) => {
const webhookUrl = process.env.SLACK_WEBHOOK_URL!;
const slackText = `*Alert: ${ruleType}*\n${message}\n` +
Object.entries(metadata).map(([k, v]) => `• ${k}: ${v}`).join("\n");
const resp = await fetch(webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text: slackText }),
});
if (!resp.ok) {
DBOS.logger.error(`Failed to send Slack alert: ${resp.status}`);
}
});
```
Example forwarding alerts to PagerDuty using the [Events API](https://developer.pagerduty.com/docs/events-api-v2-overview):
```typescript
DBOS.setAlertHandler(async (ruleType: string, message: string, metadata: Record) => {
const routingKey = process.env.PAGERDUTY_ROUTING_KEY!;
const payload = {
routing_key: routingKey,
event_action: "trigger",
payload: {
summary: `${ruleType}: ${message}`,
severity: "error",
source: "my-app",
custom_details: metadata,
},
};
const resp = await fetch("https://events.pagerduty.com/v2/enqueue", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!resp.ok) {
DBOS.logger.error(`Failed to send PagerDuty alert: ${resp.status}`);
}
});
```
See the [TypeScript reference](../typescript/reference/methods.md#dbossetalerthandler) for more details.
**Java**
Example logging alerts:
```java
import dev.dbos.transact.DBOS;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Logger logger = LoggerFactory.getLogger("AlertHandler");
dbos.registerAlertHandler((ruleType, message, metadata) -> {
logger.warn("Alert received: {} - {}", ruleType, message);
metadata.forEach((key, value) -> logger.warn(" {}: {}", key, value));
});
```
Example forwarding alerts to Slack using [incoming webhooks](https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks/)
```java
dbos.registerAlertHandler((ruleType, message, metadata) -> {
String webhookUrl = System.getenv("SLACK_WEBHOOK_URL");
String metaLines = metadata.entrySet().stream()
.map(e -> "• " + e.getKey() + ": " + e.getValue())
.collect(Collectors.joining("\n"));
String slackText = String.format("*Alert: %s*\n%s\n%s", ruleType, message, metaLines);
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(webhookUrl))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{\"text\":\"" + slackText + "\"}"))
.build();
try {
client.send(request, HttpResponse.BodyHandlers.ofString());
} catch (Exception e) {
logger.error("Failed to send Slack alert: {}", e.getMessage());
}
});
```
Example forwarding alerts to PagerDuty using the [Events API](https://developer.pagerduty.com/docs/events-api-v2-overview):
```java
dbos.registerAlertHandler((ruleType, message, metadata) -> {
String routingKey = System.getenv("PAGERDUTY_ROUTING_KEY");
String payload = String.format("""
{"routing_key":"%s","event_action":"trigger","payload":{
"summary":"%s: %s","severity":"error","source":"my-app",
"custom_details":%s}}""",
routingKey, ruleType, message, new ObjectMapper().writeValueAsString(metadata));
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://events.pagerduty.com/v2/enqueue"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
try {
client.send(request, HttpResponse.BodyHandlers.ofString());
} catch (Exception e) {
logger.error("Failed to send PagerDuty alert: {}", e.getMessage());
}
});
```
#### Metrics-Based Alerts
If you scrape [Conductor metrics](./metrics.md) into a monitoring system such as Prometheus (with [Alertmanager](https://prometheus.io/docs/alerting/latest/alertmanager/)), Datadog, or Grafana, you can define alerts directly on DBOS metrics.
This is an alternative to the Conductor-managed alerts above, useful if you already run a monitoring and alerting stack.
Here are some example alerting conditions written in [PromQL](https://prometheus.io/docs/prometheus/latest/querying/basics/):
**Elevated workflow failures:** more than 10 workflows failing per minute for an application:
```promql
sum by (application) (dbos_conductor_v1_workflow_failed_rate) * 60 > 10
```
**Stuck queue:** a workflow has been waiting in a queue for more than 5 minutes:
```promql
time() - dbos_conductor_v1_workflow_oldest_enqueued_timestamp_seconds > 300
```
**Application offline:** no healthy executors are connected for an application:
```promql
absent(dbos_conductor_v1_executor_count{application="my-app", status="HEALTHY"})
```
Detecting an offline application is a "missing data" condition: a fully disconnected application emits no executor series at all.
In PromQL, use `absent()` with an explicit `application` label for each app you want to monitor.
---
## Audit Logs
If you are using [Conductor](./overview.md), you can retrieve an **audit log** of the mutating operations performed against your organization: registering and deleting applications, managing workflows and schedules, creating and revoking API keys, changing roles and membership, and updating organization settings.
The audit log is append-only and records who did what, when, from where, and whether the operation succeeded.
:::info
Audit logs require a [DBOS Enterprise](https://www.dbos.dev/dbos-pricing) plan.
:::
### The Audit Logs Endpoint
Conductor exposes an organization's audit log through the [Conductor API](./reference/conductor-api.md) at:
```
GET https://cloud.dbos.dev/conductor/v2/orgs/{orgName}/audit-logs
```
`{orgName}` is your DBOS organization name.
The endpoint is authenticated with a Conductor API key, passed as a bearer token in the `Authorization` header.
You can generate an API key from the [key settings page](https://console.dbos.dev/settings/apikey) of the DBOS Console. **Make sure the key has the [`organization.read`](./permissions.md) permission.**
A read is a simple authenticated `GET`:
```bash
curl -G https://cloud.dbos.dev/conductor/v2/orgs/my_org/audit-logs \
-H "Authorization: Bearer $DBOS_API_KEY" \
--data-urlencode "operation=workflow.cancel" \
--data-urlencode "limit=50"
```
:::note
Audit logs are an organization-level concept, so a [self-hosted Conductor](./self-hosting/hosting-conductor.md) running with authentication disabled does not register this operation and responds `404`. See [Self-hosted differences](./reference/conductor-api.md#self-hosted-differences).
:::
Entries are returned newest first (by emit time).
#### Filtering and pagination
By default the endpoint returns the most recent entries for your organization. You can narrow the results with these query parameters, all optional:
| Parameter | Description |
| --- | --- |
| `startTime` | Only return entries at or after this time. [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339) timestamp (e.g. `2026-07-01T00:00:00Z`), inclusive. |
| `endTime` | Only return entries before this time. RFC 3339 timestamp, exclusive. |
| `operation` | Only return entries for this [operation](#operations), matched exactly (e.g. `application.delete`). |
| `subject` | Only return entries whose actor matches this value, compared against both the subject's display name (email or API-key name) and its id. |
| `target` | Only return entries whose target resource id matches this value exactly (e.g. an application name or workflow id). |
| `limit` | Maximum number of entries to return. Defaults to `100`; the maximum is `1000`, and a larger value is rejected as an error. |
| `offset` | Number of matching entries to skip. Defaults to `0`. |
Pagination is offset-based over the filtered, time-ordered results. To page through the log, hold the filters constant and advance `offset` by `limit` on each request. A page with fewer than `limit` entries means you have reached the end.
For example, to fetch the second page of application deletions in June:
```
https://cloud.dbos.dev/conductor/v2/orgs/my_org/audit-logs?operation=application.delete&startTime=2026-06-01T00:00:00Z&endTime=2026-07-01T00:00:00Z&limit=100&offset=100
```
### The Response
The endpoint returns a JSON array of audit entries:
```json
[
{
"id": "3f9a1c2e-6b0d-4f8a-9c1e-2a7b5d4c8e10",
"emitTime": "2026-07-06T18:22:41.512Z",
"operation": "workflow.cancel",
"status": "success",
"subject": {
"type": "user",
"id": "user_123",
"display": "alice@example.com"
},
"target": {
"type": "workflow",
"id": "e1b2c3d4-a5b6-7c8d-9e0f-1a2b3c4d5e6f"
},
"sourceIp": "203.0.113.7",
"details": {
"application_name": "dbos-node-toolbox"
}
}
]
```
Each entry has the following fields:
| Field | Description |
| --- | --- |
| `id` | Unique identifier of the audit entry. |
| `emitTime` | When the operation was recorded, as an RFC 3339 timestamp. |
| `operation` | The operation performed (see [Operations](#operations)). |
| `status` | `success`, or `failure` if the operation was rejected or errored (for example, a denied attempt or invalid request). |
| `subject` | Who performed the operation. |
| `subject.type` | `user` or `api_key`. |
| `subject.id` | Stable identifier of the user or API key. |
| `subject.display` | Human-readable actor: the user's email or the API key's name. Preserved even if the user or key is later deleted. |
| `target` | The resource the operation acted on. Omitted when no specific target applies. |
| `target.type` | The [type](#target-types) of the target resource. |
| `target.id` | Identifier or name of the target resource. |
| `sourceIp` | IP address the request originated from. |
| `details` | Operation-specific context, as a JSON object (see [Details](#details)). `null` when there is none. |
#### Operations
The `operation` field, and the `operation` query filter, use these values:
| Category | Operations |
| --- | --- |
| Applications | `application.create`, `application.update`, `application.delete`, `application.set_latest_version` |
| Workflows | `workflow.cancel`, `workflow.resume`, `workflow.restart`, `workflow.fork`, `workflow.fork_from_failure`, `workflow.delete`, `workflow.import`, `workflow.bulk_cancel`, `workflow.bulk_delete`, `workflow.bulk_resume` |
| Schedules | `schedule.pause`, `schedule.resume`, `schedule.backfill`, `schedule.trigger` |
| Alerting rules | `alerting_rule.create`, `alerting_rule.delete` |
| API keys | `token.create`, `token.revoke` |
| Roles | `role.create`, `role.delete`, `role.grant` |
| Organization | `organization.update`, `user.join`, `user.remove` |
#### Target types
The `target.type` field is one of: `application`, `workflow`, `schedule`, `alerting_rule`, `token`, `role`, `user`, `organization`.
#### Details
`details` carries additional, operation-specific context. Keys include:
| Key | Appears on |
| --- | --- |
| `application_name` | Any operation scoped to an application. |
| `workflow_ids` | Bulk workflow operations (`workflow.bulk_cancel`, `workflow.bulk_delete`, `workflow.bulk_resume`). |
| `private_mode` | `application.create` |
| `permissions`, `applications` | `token.create` |
| `permissions` | `role.create` |
| `role_name` | `role.grant` |
| `new_name`, `audit_log_retention_days` | `organization.update` |
### Retention
Audit entries are retained per-organization for a configurable window; entries older than the window are automatically deleted.
Retention defaults to **90 days** and can be set to any value between **7 and 3650 days**.
The retention period can be set through the DBOS Console.
---
## Autoscaling and Version Management
[Conductor](./overview.md) lets you attach autoscaling policies to your applications. An autoscaling policy computes how many executors your application needs, per application version, to drain one of your application's queues. A common example is configuring a [KEDA](https://keda.sh/) ScaledObject to size your application deployments based on queue utilization.
:::info
Autoscaling requires a [DBOS Teams](https://www.dbos.dev/dbos-pricing) plan.
:::
To use policies:
1. **Attach an autoscaling policy** to an application, naming the queue whose backlog drives the executor count.
2. **Poll the desired executor count**, either one version at a time or for all active versions at once.
All endpoints on this page are part of the [Conductor API](./reference/conductor-api.md); see that page for the base URL and authentication.
The examples below use `$CONDUCTOR` for the base URL and `$CONDUCTOR_KEY` for an [API key](./permissions.md).
### How It Works
Conductor counts the workflows that are `ENQUEUED` or `PENDING` on the policy queue, grouped by application version, and sizes each version to its own backlog:
```
desiredExecutors = ceil(queueDepth / workerConcurrency)
```
If the queue also has a global concurrency limit, the recommendation is additionally capped at `ceil(concurrency / workerConcurrency)`.
Versions matter because a queued workflow is executed only by executors running the version it was enqueued under. (Note that the latest version of your application can also dequeue workflows that have not been assigned a version yet.)
When you roll out a new version, its executors pick up new work while old version's executors must stay available until the old version's backlog drains.
Conductor therefore reports the latest version as needing at least one executor, and reports an old version at zero once nothing is left for it on the queue.
The policy queue must have a **worker concurrency** set.
Work outside the policy queue, such as workflows started directly or enqueued on another queue, is not visible to the policy.
Recommendations are computed from your application's [system database](../explanations/system-tables.md) through one of its healthy executors.
:::info
[Partitioned queues](../python/tutorials/queue-tutorial.md#partitioning-queues) are also sized by their queue-wide concurrency parameters, their per-partition limits are not considered.
:::
### Autoscaling From the Console
The **Executors** tab of your application's page on the [DBOS Console](https://console.dbos.dev) lets you install the policy:
In this view:
- **Autoscaling policy**: pick the queue whose utilization should drive the executor count. Only eligible queues are listed, each with its worker concurrency. The two optional fields, maximum old versions and maximum executors per old version, are rollout caps that let you orchestrate deployments from an operator. Editing requires the `application.write` permission.
- **Desired executors**: Conductor's live recommendation for the latest version, with the version it covers, when the backlog was observed, and the queue's backlog and per-worker limit behind the number.
- **Connected executors**: the application's executors grouped by version, latest first, each panel showing how many are healthy and how many Conductor wants for that version.
### Attaching a Policy with the API
Attach a policy with a `PUT`:
```shell
curl -X PUT "$CONDUCTOR/v2/orgs/{orgName}/apps/{appName}/autoscaling-policy" \
-H "Authorization: Bearer $CONDUCTOR_KEY" \
-H "Content-Type: application/json" \
-d '{"queue": "orders"}'
```
Conductor validates the queue against a running executor before storing the policy and echoes the policy back as stored:
```json
{
"policy": {
"queue": "orders"
}
}
```
The policy has one required field and an optional `rollout` section governing how old versions are sized:
| Field | Description |
| --- | --- |
| `queue` | The queue whose utilization should drive the desired executor count. It must exist and have a worker concurrency set. |
| `rollout.maxOldApplicationVersions` | How many old application versions the [all-versions endpoint](#all-versions-at-once) may include, newest first. Defaults to `0`, which means that only the latest version is reported. |
| `rollout.maxExecutorsForOldApplicationVersions` | Cap every old version's recommendation at this many executors, regardless of its backlog. `0` is valid and reports old versions at zero. Omit to size old versions from their own backlog, uncapped. |
For example, this policy keeps at most two old versions running, with at most one executor each, so most capacity goes to the latest version during a rollout:
```json
{
"queue": "orders",
"rollout": {
"maxOldApplicationVersions": 2,
"maxExecutorsForOldApplicationVersions": 1
}
}
```
`GET` the same path to read the stored policy (`404` when none is set), and `DELETE` it to turn autoscaling off.
Setting and deleting a policy require the `application.write` permission and are recorded in the [audit log](./audit-logs.md).
### Reading the Desired Executor Count
Conductor exposes two endpoints to read scaling recommendation: one version at a time, which suits an autoscaler like KEDA, or all versions at once, which suits an operator managing deployments.
Both read endpoints return the same recommendation object per version:
| Field | Description |
| --- | --- |
| `applicationVersion` | The application version this recommendation covers. |
| `isLatest` | `true` for the application's latest registered version. |
| `desiredExecutors` | How many executors of this version are needed to satisfy the queue load at the time of the reading. |
| `queueName` | The queue the stored policy scales on. |
| `queueDepth` | The `ENQUEUED` and `PENDING` backlog counted for this version on the policy queue. |
| `observedAt` | When the backlog was measured, in epoch milliseconds. |
#### One Version at a Time
```shell
curl -H "Authorization: Bearer $CONDUCTOR_KEY" \
"$CONDUCTOR/v2/orgs/{orgName}/apps/{appName}/autoscale/versions/latest"
```
```json
{
"applicationVersion": "1787155000092755696-000b07ce8f114cc4",
"isLatest": true,
"desiredExecutors": 4,
"queueName": "orders",
"queueDepth": 12,
"observedAt": 1787155105672
}
```
The `{version}` path parameter is either `latest` or any version the application has registered.
`latest` always resolves to whichever version is currently latest, so a single poller pointed at it keeps working across rollouts with no reconfiguration.
The latest version is always reported as needing at least one executor.
An old version is reported at zero once it has no work left on the queue, which signals that its executors could be torn down.
The policy's `maxExecutorsForOldApplicationVersions` cap applies to old versions, and `maxOldApplicationVersions` does not apply to this endpoint, since it only ever reports the version you asked for.
This endpoint is made to be polled per deployment, for example by one KEDA ScaledObject per version's Deployment.
The response is an absolute executor count, so configure your autoscaler to map it one-to-one to replicas.
#### All Versions at Once
```shell
curl -H "Authorization: Bearer $CONDUCTOR_KEY" \
"$CONDUCTOR/v2/orgs/{orgName}/apps/{appName}/autoscale"
```
```json
[
{
"applicationVersion": "1787155000092755696-000b07ce8f114cc4",
"isLatest": true,
"desiredExecutors": 4,
"queueName": "orders",
"queueDepth": 12,
"observedAt": 1787155105672
},
{
"applicationVersion": "1787140000012345678-9f0e1d2c3b4a5968",
"isLatest": false,
"desiredExecutors": 1,
"queueName": "orders",
"queueDepth": 2,
"observedAt": 1787155105672
}
]
```
This endpoint returns one entry per version that should be running:
- The latest version comes first and is always present, at one executor when it has no work.
- It is followed by at most `maxOldApplicationVersions` old versions that still have work on the queue, most recently registered first.
- An old version with no remaining work is omitted. Its absence is the signal that its deployment can be deleted.
- `maxExecutorsForOldApplicationVersions` caps every old version's desired executors count.
This shape suits a controller that owns the full set of deployments: it can create a deployment for each version in the response, size each to its `desiredExecutors`, and delete any deployment whose version is no longer listed.
### Errors
| Status | Meaning |
| --- | --- |
| `400` | The policy names no queue, a queue the application does not define, or a queue that has no worker concurrency. |
| `404` | The application has no autoscaling policy, or the requested version was never registered. |
| `502` / `503` | No healthy executor of the application is connected, or every executor failed to answer. |
:::warning
A stored policy can become invalid if you later change the queue's definition.
:::
---
## Distributed Recovery
If your application is connected to [DBOS Conductor](./overview.md), workflow recovery is automatic.
When Conductor detects that an executor is unhealthy, it automatically signals another executor to recover its workflows.
When an executor disconnects from Conductor, its status is changed to `DISCONNECTED` while Conductor waits for it to reconnect.
If it has not reconnected after a certain period of time, its status is changed to `DEAD` and Conductor signals another executor to recover its workflows.
After recovery is confirmed, Conductor deletes its record of the executor.
By default, the executor timeout is 60 seconds, so Conductor waits 60 seconds after an executor disconnects before recovering its workflows.
You can configure the executor timeout per application from the DBOS Console.
---
## Metrics
If you are using [Conductor](./overview.md), you can scrape metrics about your applications' workflows, steps, and executors from a [Prometheus](https://prometheus.io/)-compatible endpoint.
This lets you monitor your DBOS applications in Prometheus, Grafana, or any other tool that understands the [OpenMetrics](https://prometheus.io/docs/specs/om/open_metrics_spec/) format.
:::info
Metrics require at least a [DBOS Teams](https://www.dbos.dev/dbos-pricing) plan.
:::
:::info
Metrics require DBOS Python >=2.23.0 or DBOS TypeScript >=4.19.
:::
### The Metrics Endpoint
Conductor exposes metrics for all of your applications at a single Prometheus-compatible OpenMetrics scrape endpoint:
```
https://cloud.dbos.dev/v1/metrics
```
The endpoint is authenticated with a Conductor API key, passed as a bearer token in the `Authorization` header.
You can generate an API key from the [key settings page](https://console.dbos.dev/settings/apikey) of the DBOS Console. **Make sure to enable the metrics read permission for the key.**
A scrape is a simple authenticated `GET`:
```bash
curl https://cloud.dbos.dev/v1/metrics \
-H "Authorization: Bearer $DBOS_API_KEY" \
-H "Accept: application/openmetrics-text"
```
#### Endpoint Integrations
The endpoint works with any tool that can scrape the OpenMetrics or Prometheus exposition format.
**Prometheus**
To scrape the endpoint from Prometheus, add a job like the following to your `prometheus.yml`. Store your API key in a file and reference it with `authorization.credentials_file` (or use `credentials` directly):
```yaml
scrape_configs:
- job_name: dbos
scheme: https
metrics_path: /v1/metrics
scrape_interval: 60s
honor_timestamps: true
static_configs:
- targets: ["cloud.dbos.dev"]
authorization:
type: Bearer
credentials_file: /etc/prometheus/dbos_api_key
```
Set `honor_timestamps: true` so the window timestamps the endpoint emits are preserved.
**Datadog**
To collect the metrics with Datadog, use the [OpenMetrics integration](https://docs.datadoghq.com/integrations/openmetrics/) built into the Datadog Agent. Add an instance like the following to `conf.d/openmetrics.d/conf.yaml`, then restart the Agent:
```yaml
instances:
- openmetrics_endpoint: https://cloud.dbos.dev/v1/metrics
namespace: dbos
# Collect no more than once per minute (see "Aggregation Window" below).
min_collection_interval: 60
metrics:
- "dbos_conductor_v1_.*"
headers:
Authorization: "Bearer "
Accept: application/openmetrics-text
```
**OpenTelemetry Collector**
The [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/)'s [`prometheus` receiver](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/prometheusreceiver) scrapes the endpoint and forwards the metrics to any backend you configure an exporter for. It takes a standard Prometheus `scrape_configs` block, so store your API key in a file and reference it with `authorization.credentials_file`:
```yaml
receivers:
prometheus:
config:
scrape_configs:
- job_name: dbos
scheme: https
metrics_path: /v1/metrics
# Scrape no more than once per minute (see "Aggregation Window" below).
scrape_interval: 60s
honor_timestamps: true
static_configs:
- targets: ["cloud.dbos.dev"]
authorization:
type: Bearer
credentials_file: /etc/otelcol/dbos_api_key
exporters:
# Configure an exporter for your observability backend.
otlphttp:
endpoint: https://your-backend.example.com
service:
pipelines:
metrics:
receivers: [prometheus]
exporters: [otlphttp]
```
Set `honor_timestamps: true` so the window timestamps the endpoint emits are preserved.
#### Filtering metrics
By default the endpoint returns every metric for every application in your organization. You can narrow a scrape with these repeatable query parameters:
| Parameter | Description |
| --- | --- |
| `applications` | Only report metrics for the named application(s). Matched exactly. |
| `workflow_names` | Only report metrics for the named workflow(s). |
| `metrics` | Only emit the named metric families (e.g. `dbos_conductor_v1_workflow_success_rate`). |
Each parameter may be repeated to select multiple values, for example:
```
https://cloud.dbos.dev/v1/metrics?applications=my-app&applications=my-other-app
```
### Available Metrics
Every metric this endpoint emits is an OpenMetrics **gauge**. All metric names are prefixed with `dbos_conductor_v1_`, and every series carries an `application` label.
If multiple applications [share a system database](../explanations/sharing-a-system-database.md), each application's metrics count only the workflows and steps it owns.
#### Aggregation window
Each scrape reports data for the **most recently completed clock-aligned minute**.
For example, a scrape at any time during `12:34` reports data aggregated over the window `[12:33:00, 12:34:00)`.
Although every metric is a gauge, the value a gauge carries falls into one of three flavors, noted in the **Measurement** column below:
- **Rate** — a per-second average over the window. For example, if 120 workflows succeeded in the window, `workflow_success_rate` reports `2`; multiply by 60 to recover the count over the minute. Because these are already-averaged gauges (not counters), do **not** wrap them in PromQL `rate()`.
- **Point-in-time** — the value at scrape time, not tied to the window (for example, the number of workflows currently enqueued).
- **Windowed** — an aggregate, such as a maximum, computed over the window.
Rate and windowed metrics are stamped with the window's timestamp (so scrapes within the same minute deduplicate); point-in-time metrics carry no explicit timestamp and use the scrape time.
The windowed metrics (`workflow_max_queue_wait_seconds`, `workflow_max_total_latency_seconds`, and `step_max_duration_seconds`) report a **maximum per label group**. When you combine groups in a query, aggregate them with `max()` — a maximum of maximums is still a maximum — rather than `sum()` or `avg()`, which are not meaningful over these values.
#### Workflow metrics
These metrics are labeled by `workflow_name` and, where noted, `queue_name`.
| Metric | Measurement | Description |
| --- | --- | --- |
| `workflow_started_rate` | Rate | Workflows created per second. Labeled by queue. |
| `workflow_dequeued_rate` | Rate | Enqueued workflows dequeued per second. Workflows that were never enqueued are not counted. Labeled by queue. |
| `workflow_success_rate` | Rate | Workflows that completed successfully per second. Labeled by queue. |
| `workflow_failed_rate` | Rate | Workflows that terminated with an error (`ERROR` or `MAX_RECOVERY_ATTEMPTS_EXCEEDED`) per second. Labeled by queue. |
| `workflow_cancelled_rate` | Rate | Workflows that were cancelled per second. Labeled by queue. |
| `workflow_enqueued_count` | Point-in-time | Workflows currently in the `ENQUEUED` state. Labeled by queue. |
| `workflow_pending_count` | Point-in-time | Workflows currently in the `PENDING` (executing) state. |
| `workflow_oldest_enqueued_timestamp_seconds` | Point-in-time | Unix timestamp (seconds) of the oldest workflow currently `ENQUEUED`. Use `time() - ` to derive its age. No series is emitted when no workflows are enqueued. Labeled by queue. |
| `workflow_oldest_pending_timestamp_seconds` | Point-in-time | Unix timestamp (seconds) of the oldest workflow currently `PENDING`. Use `time() - ` to derive its age. No series is emitted when no workflows are pending. |
| `workflow_max_queue_wait_seconds` | Windowed | Maximum queue wait (created to first started), in seconds, across workflows that completed successfully in the window. Labeled by queue. |
| `workflow_max_total_latency_seconds` | Windowed | Maximum end-to-end latency (created to completed), in seconds, across workflows that completed successfully in the window. Labeled by queue. |
#### Step metrics
These metrics are labeled by `step_name`.
| Metric | Measurement | Description |
| --- | --- | --- |
| `step_success_rate` | Rate | Workflow steps that completed successfully per second. |
| `step_failed_rate` | Rate | Workflow steps that terminated with an error per second. |
| `step_max_duration_seconds` | Windowed | Maximum single-step duration, in seconds, across steps that completed successfully in the window. |
#### Executor metrics
| Metric | Measurement | Description |
| --- | --- | --- |
| `executor_count` | Point-in-time | Number of executors registered for the application, labeled by `status` and `application_version`. |
### Example Queries
A few example [PromQL](https://prometheus.io/docs/prometheus/latest/querying/basics/) queries:
```promql
# Number of workflows that completed successfully in the past hour, across all workflows and queues.
# workflow_success_rate is a per-second gauge, so average it over the hour and multiply by 3600 seconds.
sum(avg_over_time(dbos_conductor_v1_workflow_success_rate{application="my-app"}[1h])) * 3600
# Age, in seconds, of the oldest currently enqueued workflow
time() - dbos_conductor_v1_workflow_oldest_enqueued_timestamp_seconds
# Number of healthy executors per application
sum by (application) (dbos_conductor_v1_executor_count{status="HEALTHY"})
```
---
## DBOS Conductor Overview
When operating DBOS durable workflows in production, we strongly recommend connecting your application to Conductor.
Conductor is the control plane for your durable workflows, providing:
- [**High availability**](./distributed-recovery.md): In a distributed environment with many executors running durable workflows, Conductor automatically detects when a workflow is interrupted (for example, if its executor disconnects or crashes) and recovers the workflow to another healthy executor.
- [**Workflow and queue observability**](./workflow-management.md): Conductor provides dashboards of all active and past workflows and all queued tasks as well as real-time workflow visualization.
- [**Workflow and queue management**](./workflow-management.md): From the Conductor dashboard, you can pause any workflow execution, start any stopped or enqueued workflow, or restart any workflow from a specific step. This is useful for rapidly responding to incidents or debugging.
- [**Managed Retention Policies**](./retention.md): From the Conductor dashboard, manage how much workflow history each of your applications should retain and for how long to retain it.
- [**Autoscaling and version management**](./autoscaling.md): Conductor computes how many executors each version of your application needs from queue utilization, so autoscalers like KEDA can size a deployment per application version, drain old versions down to zero, and drive rollouts.
- [**Observability Integrations**](./metrics.md): Conductor exposes metrics about your applications' workflows, steps, and executors from a Prometheus-compatible endpoint, so you can monitor your DBOS applications in Datadog, Grafana, or any other tool that understands the OpenMetrics format.
- [**Programmatic access**](./reference/conductor-api.md): Conductor's workflow, queue, and schedule management is available over an OpenAPI-described HTTP API and from the [`dbosctl` command-line client](./reference/dbosctl.md), so you can script incident response and wire Conductor into your own tooling.
Architecturally, Conductor is not part of your workflows orchestration path.
If your connection to Conductor is interrupted, your applications will continue operating normally.
Recovery, observability, and workflow management will automatically resume once connectivity is restored.
### Connecting To Conductor
To connect your application to Conductor, first register your application on the [DBOS Console](https://console.dbos.dev).
**The name you register must match the name you give your application in its configuration.**
Next, generate an API key from the [key settings page](https://console.dbos.dev/settings/apikey).
By default, API keys do not expire, though they may be revoked at any time.
Finally, supply that API key to your DBOS application to connect it to Conductor.
This initiates a websocket connection with Conductor:
:::tip
The application name in your DBOS configuration must match the name with which you registered your app in Conductor.
The name also identifies the application's data in its system database; see [sharing a system database](../explanations/sharing-a-system-database.md) for more information.
:::
**Python**
```python
config: DBOSConfig = {
"name": "my-app-name",
"application_version": "0.1.0",
"system_database_url": os.environ.get("DBOS_SYSTEM_DATABASE_URL"),
"conductor_key": os.environ.get("DBOS_CONDUCTOR_KEY")
}
DBOS(config=config)
```
**TypeScript**
```typescript
DBOS.setConfig({
"name": "my-app-name",
"applicationVersion": "0.1.0",
"systemDatabaseUrl": process.env.DBOS_SYSTEM_DATABASE_URL,
});
const conductorKey = process.env.DBOS_CONDUCTOR_KEY
await DBOS.launch({conductorKey})
```
**Go**
```go
conductorKey := os.Getenv("DBOS_CONDUCTOR_KEY")
dbosContext, err := dbos.NewDBOSContext(context.Background(), dbos.Config{
AppName: "dbos-starter",
ApplicationVersion: "0.1.0",
DatabaseURL: os.Getenv("DBOS_SYSTEM_DATABASE_URL"),
ConductorAPIKey: conductorKey,
})
```
**Java**
```java
String conductorKey = System.getenv("DBOS_CONDUCTOR_KEY");
DBOSConfig config = DBOSConfig.defaults("dbos-java-starter")
.withAppVersion("0.1.0")
.withDatabaseUrl(System.getenv("DBOS_SYSTEM_JDBC_URL"))
.withConductorKey(conductorKey)
```
### Managing Conductor Applications
You can view all applications registered with Conductor on the DBOS Console:
On your application's page, you can see all executors (processes) running that application that are currently connected to Conductor.
Executors are identified by a unique ID that they generate and print on startup.
When you restart an executor, it generates a new ID.
You can tag executors with custom metadata (such as region or instance type) using the `conductor_executor_metadata` configuration option (in TypeScript, the `conductorExecutorMetadata` launch option). This metadata is displayed on the dashboard to help you identify executors.
Conductor uses a WebSocket-based protocol to exchange workflow metadata and commands with your application. An application is shown as _available_ in Conductor when at least one of its processes is connected. Conductor has no access to your application's database or other private data. As a result, workflow-related features are only available while your application is connected to Conductor over this metadata-only connection.
:::tip
For isolation, you should set up a separate Conductor app for each environment in which you run your DBOS application.
For example, you may want to have separate dev, staging, and prod Conductor apps.
To facilitate this, pass in your application name as an environment variable, for example:
**Python**
```python
config: DBOSConfig = {
"name": os.environ.get("DBOS_APPLICATION_NAME"),
"application_version": "0.1.0",
"system_database_url": os.environ.get("DBOS_SYSTEM_DATABASE_URL"),
"conductor_key": os.environ.get("DBOS_CONDUCTOR_KEY")
}
DBOS(config=config)
```
**TypeScript**
```typescript
DBOS.setConfig({
"name": process.env.DBOS_APPLICATION_NAME!,
"applicationVersion": "0.1.0",
"systemDatabaseUrl": process.env.DBOS_SYSTEM_DATABASE_URL,
});
const conductorKey = process.env.DBOS_CONDUCTOR_KEY
await DBOS.launch({conductorKey})
```
**Go**
```go
conductorKey := os.Getenv("DBOS_CONDUCTOR_KEY")
dbosContext, err := dbos.NewDBOSContext(context.Background(), dbos.Config{
AppName: os.Getenv("DBOS_APPLICATION_NAME"),
ApplicationVersion: "0.1.0",
DatabaseURL: os.Getenv("DBOS_SYSTEM_DATABASE_URL"),
ConductorAPIKey: conductorKey,
})
```
**Java**
```java
String appName = System.getenv("DBOS_APPLICATION_NAME")
String conductorKey = System.getenv("DBOS_CONDUCTOR_KEY");
DBOSConfig config = DBOSConfig.defaults(appName)
.withAppVersion("0.1.0")
.withDatabaseUrl(System.getenv("DBOS_SYSTEM_JDBC_URL"))
.withConductorKey(conductorKey)
```
:::
#### Metadata-Only Mode
:::info
Metadata-Only mode requires at least a [DBOS Teams](https://www.dbos.dev/dbos-pricing) plan.
:::
If an application handles especially sensitive data, you may consider enabling metadata-only mode for it.
In this mode, only workflow and step metadata (but not data, like workflow or step inputs or outputs) is sent to Conductor.
As a result, workflow data will not be visible from the console.
Note that Conductor does not store application data in any mode.
You can also enable metadata-only mode from your application, so it is enforced by the application process regardless of the setting in the console, by setting `conductor_metadata_only_mode` in your [Python configuration](../python/reference/configuration.md#conductor-settings) or `conductorMetadataOnlyMode` in your [TypeScript launch options](../typescript/reference/dbos-class.md#dboslaunch).
---
## Permissions and API Keys
DBOS Conductor controls access to your organization's applications, workflows, and settings using **role-based access control (RBAC)** for users and **scoped API keys** for applications and automation.
This page describes the permission model, the built-in and custom roles, and how to create and manage API keys.
You manage permissions and API keys from the [DBOS Console](https://console.dbos.dev).
### Permissions
Every action in Conductor, like viewing a workflow, registering an application, or creating an API key, requires a specific permission.
| Permission | Grants the ability to |
| --- | --- |
| `organization.read` | View organization details, members, and roles. |
| `organization.write` | Manage the organization: rename it, add and remove members, create and assign roles, manage billing.
| `application.read` | View applications and their workflows, queues, schedules, executors, and alerting rules. |
| `application.write` | Register, update, and delete applications; manage workflows (cancel, resume, fork, delete, import); and manage schedules and alerting rules. |
| `metric.read` | Read application metrics, including the [Prometheus-compatible metrics endpoint](./metrics.md). |
| `token.read` | List API keys. |
| `token.write` | Create and revoke API keys. |
| `websocket.connect` | For an API key, connect a running application executor to Conductor over its websocket. |
### Roles
A **role** is a named set of permissions.
Each member of an organization is assigned exactly one role, which determines everything they can do in that organization.
#### Built-in roles
Every organization has two built-in roles:
| Permission | Organization Member | Organization Admin |
| --- | :---: | :---: |
| `organization.read` | ✅ | ✅ |
| `organization.write` | | ✅ |
| `application.read` | ✅ | ✅ |
| `application.write` | ✅ | ✅ |
| `metric.read` | ✅ | ✅ |
| `token.read` | ✅ | ✅ |
| `token.write` | ✅ | ✅ |
| `websocket.connect` | ✅ | ✅ |
- **Organization Admin** holds every permission. Admins can manage the organization and its members and roles, in addition to managing applications, API keys, and metrics.
- **Organization Member** holds every permission *except* `organization.write`. Members can manage applications, workflows, and API keys and view metrics, but cannot change organization settings, manage members, or manage roles.
Built-in roles are shared by every organization. They cannot be deleted or renamed.
#### Custom roles
:::info
Custom roles require at least a [DBOS Teams](https://www.dbos.dev/dbos-pricing) plan.
:::
Organization admins can create **custom roles** with any combination of permissions.
This is useful for granting narrower access than the built-in roles; for example, a read-only role that can view applications and metrics but not modify them.
You can manage custom roles from the [organization settings](https://console.dbos.dev/settings/organization) page in the console.
#### Assigning roles to members
Organization admins manage members and their roles from the [organization settings](https://console.dbos.dev/settings/organization) page in the console:
- **Change a member's role** to any role whose permissions the admin also holds.
- **Remove a member** from the organization.
Changing a member's role replaces their previous role. A member always has exactly one role at a time.
### API keys
An **API key** authenticates a non-human caller.
API keys are used by running DBOS applications connecting to Conductor, but can also be used by scripts and CI/CD automation that call the Conductor API.
Like a role, every API key carries a set of permissions.
They can also be scoped to specific applications.
API keys do not expire, but can be revoked at any time.
A key can be renamed after creation without changing its secret, from the console, with [`dbosctl api-key rename`](./reference/dbosctl.md#dbosctl-api-key-rename), or through the [Conductor API](./reference/conductor-api.md#roles-permissions-and-api-keys).
#### Permissions and application scope
An API key has two independent restrictions:
- **Permissions** — the set of capabilities the key grants, drawn from the same [permission catalog](#permissions) as roles.
- **Application scope** — either *all applications* in the organization (org-wide), or a specific list of applications. A key scoped to specific applications is rejected on any request targeting an application outside its list, even if it holds the required permission.
For example, an API key with only `application.read` scoped to a single application can read that application's workflows and nothing else.
#### Using an API key
Supply the key to your DBOS application to connect it to Conductor, as described in [Connecting to Conductor](./overview.md#connecting-to-conductor).
You can also use an API key to authenticate HTTP calls to the Conductor API (for example the [metrics endpoint](./metrics.md)), passing the key as a bearer token:
```
Authorization: Bearer dbos_...
```
---
## Conductor API
Conductor is the control plane for your durable workflows, and this HTTP API is how you drive it programmatically: register applications with Conductor and tune their settings, search workflows, cancel or fork them, inspect queues and schedules, drive schedules, read metrics and audit logs, and manage members, roles, and API keys.
This is the Conductor half of the [DBOS Console](https://console.dbos.dev) — what the console shows for an application connected to Conductor, whether that application runs on your own infrastructure or on DBOS Cloud. DBOS Cloud's own operations, such as [deploying an application](./dbos-cloud/deploying-to-cloud.md) or [provisioning a database](./dbos-cloud/database-management.md), are not part of this API; they have their own [CLI](./dbos-cloud/cloud-cli.md).
The API is described by an OpenAPI 3.1 specification generated directly from the running server, so it is never out of date with the deployment serving it. Both the console and the [`dbosctl` CLI](./dbosctl.md) drive Conductor through this API, using clients generated from that spec.
### Base URL
| Deployment | Base URL |
| --- | --- |
| DBOS-hosted Conductor | `https://cloud.dbos.dev/conductor` |
| [Self-hosted Conductor](../self-hosting/hosting-conductor.md) | `http://:8090` (port `8090` by default) |
Every path is relative to that base, so the full URL of an operation is, for example:
```
https://cloud.dbos.dev/conductor/v2/orgs/my_org/apps/my-app/workflows
```
The paths themselves are identical in both deployments; only the base differs.
### The OpenAPI Specification
There are three ways to obtain the spec.
**From DBOS-hosted Conductor.** The spec is served publicly (no authentication required) and reflects the currently deployed version:
```shell
curl -O https://cloud.dbos.dev/conductor/v2/openapi.json
```
If your toolchain does not yet support OpenAPI 3.1, request the 3.0 downgrade instead:
```shell
curl -O https://cloud.dbos.dev/conductor/v2/openapi-3.0.json
```
The spec served here is Conductor's own, with only its `servers` entry repointed at `/conductor` so that generated clients resolve paths correctly through DBOS-hosted Conductor.
**From a self-hosted Conductor.** The server mounts the spec and an interactive browser at its root, all unauthenticated:
| Path | Serves |
| --- | --- |
| `/openapi.json` | OpenAPI 3.1 specification (JSON) |
| `/openapi.yaml` | The same specification in YAML |
| `/openapi-3.0.json` | OpenAPI 3.0 downgrade |
| `/docs` | Interactive API browser |
| `/schemas/*` | The JSON Schema documents referenced by the spec |
For example, with the Docker Compose setup from the [Self-Hosting Guide](../self-hosting/hosting-conductor.md), open `http://localhost:8090/docs` to explore the API in your browser.
**From the Conductor image.** Conductor's `openapi` subcommand prints the spec to stdout without connecting to a database or requiring any runtime configuration, which is convenient in CI and code generation pipelines. The image's entrypoint starts the server, so override it to reach the subcommand:
```shell
docker run --rm --entrypoint ./dbos-conductor dbosdev/conductor:latest \
openapi > openapi.json
docker run --rm --entrypoint ./dbos-conductor dbosdev/conductor:latest \
openapi -spec-version 3.0 > openapi-3.0.json
```
Pin a version tag rather than `latest` when the spec feeds code generation, so a regenerated client changes only when you choose to bump it.
:::info
This emits the *complete* route surface, including operations that a no-auth deployment does not register. See [Self-hosted differences](#self-hosted-differences) below.
:::
### Authentication
All authenticated requests carry a bearer token:
```
Authorization: Bearer
```
Conductor accepts two kinds of token, distinguished by their prefix:
| Credential | Description |
| --- | --- |
| **API key** | A key beginning with `dbos_`, created with `POST /v2/orgs/{orgName}/tokens/{tokenName}` (or from the console, or with [`dbosctl api-key create`](./dbosctl.md#dbosctl-api-key-create)). Keys authenticate machine-to-machine callers and can be scoped to specific applications and permissions. A key carries no user identity, so it cannot call `GET /v2/users/me`. |
| **User JWT** | An OIDC-issued JSON Web Token identifying a human user. This is what the console and `dbosctl login` use. |
Both are sent the same way; Conductor tells them apart by the `dbos_` prefix.
Authorization is enforced per operation using the permission model described in [Permissions and API Keys](../permissions.md) — a caller needs `application.read` to list workflows, `application.write` to cancel one, `organization.write` to manage members, and so on. An unauthenticated request returns `401`; an authenticated request lacking the required permission returns `403`.
### Resource Naming
Almost every operation is scoped to an organization, and most are additionally scoped to an application:
```
/v2/orgs/{orgName}/apps/{appName}/workflows/{workflowId}
```
| Path parameter | Constraints |
| --- | --- |
| `orgName` | 3–30 characters, matching `^[a-z0-9_]+$` |
| `appName` | 3–256 characters, matching `^[a-z0-9-_]+$` |
Only two operations sit outside an organization: `POST /v2/users` and `GET /v2/users/me`.
### How Operations Are Served
Conductor answers some requests from its own database and delegates the rest to your running application. Which one applies is a property of the resource, not of whether the operation reads or writes:
| Served from | Operations |
| --- | --- |
| Conductor's database | Users, organizations, roles, permissions, and API keys; application registration, settings, and executor listing; alerting rules, audit logs, and metrics |
| Your application | Everything under workflows, queues, and schedules — reads as much as mutations — plus listing application versions and setting the latest one |
Conductor dispatches the second group over the websocket each executor holds open, waits for the reply, and returns it. Those operations therefore need a healthy executor connected for the target application, and they read whatever that executor's system database holds — Conductor neither caches nor mirrors it.
They fail with `503` when the application has no healthy executor connected, and `502` when every healthy executor fails to answer. A `503` from `GET .../workflows` means your application is not connected, not that it has no workflows.
### Errors
Errors are returned as [RFC 9457 problem details](https://www.rfc-editor.org/rfc/rfc9457) with content type `application/problem+json`:
```json
{
"status": 404,
"title": "Not Found",
"detail": "workflow 8f4a1e0c-1b2d-4c9a-a3e5-77d2c9a1b6ef not found",
"type": "about:blank"
}
```
Validation failures add an `errors` array locating each individual problem:
```json
{
"status": 422,
"title": "Unprocessable Entity",
"detail": "validation failed",
"errors": [
{ "location": "body.limit", "message": "expected integer", "value": "ten" }
]
}
```
Common statuses:
| Status | Meaning |
| --- | --- |
| `400` / `422` | Malformed request or failed validation |
| `401` | Missing, expired, or invalid credentials |
| `403` | Authenticated, but lacking the required permission |
| `404` | No such organization, application, or resource — or an operation not registered in this deployment mode |
| `502` / `503` | The application serving this resource failed to answer, or has no healthy executor connected — see [How Operations Are Served](#how-operations-are-served) |
### Listing and Filtering
List operations that can return large result sets accept `limit` and `offset` query parameters for paging, and `sortDesc=true` to return newest results first. Time windows are given as `startTime` and `endTime` in RFC 3339 format.
Workflow listing comes in two flavors:
- **`GET .../workflows`** takes a few common filters as query parameters — `status`, `workflowName`, `limit`, `offset`, `sortDesc`, `loadInput`, `loadOutput` — and is convenient for quick queries.
- **`POST .../workflows/search`** takes a JSON body and supports the full filter set the console uses: arrays of `status`, `workflowName`, `workflowIds`, `workflowIdPrefix`, `queueName`, `scheduleName`, `user`, `executorId`, `appVersion`, `parentWorkflowId`, and `forkedFrom`, plus `startTime`/`endTime`, `completedAfter`/`completedBefore`, `dequeuedAfter`/`dequeuedBefore`, `hasParent`, `wasForkedFrom`, `queuesOnly`, `attributes`, and the same paging and sorting fields.
Workflow inputs and outputs can be large, so they are omitted unless you ask for them with `loadInput` and `loadOutput`.
### Endpoint Reference
The tables below are a map of the whole API. The generated spec is the authoritative reference for request and response schemas of each operation.
#### Users and organizations
| Operation | Endpoint |
| --- | --- |
| Register user | `POST /v2/users` |
| Get current user | `GET /v2/users/me` |
| Get organization | `GET /v2/orgs/{orgName}` |
| Update organization | `PATCH /v2/orgs/{orgName}` |
| Join organization | `POST /v2/orgs/{orgName}/join` |
| Generate join secret | `POST /v2/orgs/{orgName}/secrets` |
| List members | `GET /v2/orgs/{orgName}/members` |
| Remove member | `DELETE /v2/orgs/{orgName}/members/{username}` |
| List domain claims | `GET /v2/orgs/{orgName}/domain-claims` |
| Claim a domain | `POST /v2/orgs/{orgName}/domain-claims` |
| Release a domain claim | `DELETE /v2/orgs/{orgName}/domain-claims/{domain}` |
A **domain claim** automatically adds users who register with an email at that domain to your organization. On DBOS-hosted Conductor a claim takes effect only after DBOS approves it; on a self-hosted deployment it takes effect immediately. Claims apply to new registrations only: approving one never moves users who already have accounts, and releasing one never removes them.
#### Roles, permissions, and API keys
| Operation | Endpoint |
| --- | --- |
| List grantable permissions | `GET /v2/orgs/{orgName}/permissions` |
| List roles | `GET /v2/orgs/{orgName}/roles` |
| Create role | `POST /v2/orgs/{orgName}/roles` |
| Delete role | `DELETE /v2/orgs/{orgName}/roles/{roleName}` |
| Grant role to a member | `PUT /v2/orgs/{orgName}/members/{username}/roles/{roleName}` |
| List API keys | `GET /v2/orgs/{orgName}/tokens` |
| Create API key | `POST /v2/orgs/{orgName}/tokens/{tokenName}` |
| Rename API key | `PATCH /v2/orgs/{orgName}/tokens/{tokenName}` |
| Delete API key | `DELETE /v2/orgs/{orgName}/tokens/{tokenName}` |
Create an API key with an optional body scoping it to particular applications and permissions; omitting a field leaves that dimension unscoped:
```json
{
"appNames": ["my-app"],
"permissions": ["application.read", "metric.read"]
}
```
The response contains the key's secret. It is returned **once**, at creation, and cannot be retrieved afterwards. A key can be renamed afterwards with `PATCH /v2/orgs/{orgName}/tokens/{tokenName}` and a body of `{"newName": "..."}`; the secret itself never changes, so rotating it means deleting the key and creating a new one. See [Permissions and API Keys](../permissions.md) for the full list of permissions.
#### Applications
| Operation | Endpoint |
| --- | --- |
| List applications | `GET /v2/orgs/{orgName}/apps` |
| Get application | `GET /v2/orgs/{orgName}/apps/{appName}` |
| Register application | `PUT /v2/orgs/{orgName}/apps/{appName}` |
| Update application | `PATCH /v2/orgs/{orgName}/apps/{appName}` |
| Delete application | `DELETE /v2/orgs/{orgName}/apps/{appName}` |
| List versions | `GET /v2/orgs/{orgName}/apps/{appName}/versions` |
| Set latest version | `PATCH /v2/orgs/{orgName}/apps/{appName}/versions/latest` |
| List executors | `GET /v2/orgs/{orgName}/apps/{appName}/executors` |
| List metrics | `GET /v2/orgs/{orgName}/apps/{appName}/metrics` |
`PATCH .../apps/{appName}` is where an application's tuning settings live: the executor timeout, the global workflow timeout, the [workflow retention thresholds](../retention.md), and private mode.
:::info
`GET .../metrics` returns metrics for one application over a time window. If you want to scrape Conductor from Prometheus, Datadog, or Grafana, use the OpenMetrics endpoint described in [Metrics](../metrics.md) instead.
:::
#### Workflows
| Operation | Endpoint |
| --- | --- |
| List workflows | `GET /v2/orgs/{orgName}/apps/{appName}/workflows` |
| Search workflows | `POST /v2/orgs/{orgName}/apps/{appName}/workflows/search` |
| Workflow aggregates | `POST /v2/orgs/{orgName}/apps/{appName}/workflows/aggregates` |
| Step aggregates | `POST /v2/orgs/{orgName}/apps/{appName}/steps/aggregates` |
| Get workflow | `GET /v2/orgs/{orgName}/apps/{appName}/workflows/{workflowId}` |
| List steps | `GET .../workflows/{workflowId}/steps` |
| List events | `GET .../workflows/{workflowId}/events` |
| List notifications | `GET .../workflows/{workflowId}/notifications` |
| List streams | `GET .../workflows/{workflowId}/streams` |
| Cancel workflow | `POST .../workflows/{workflowId}/cancel` |
| Resume workflow | `POST .../workflows/{workflowId}/resume` |
| Fork workflow | `POST .../workflows/{workflowId}/fork` |
| Delete workflow | `DELETE .../workflows/{workflowId}` |
| Export workflow | `GET .../workflows/{workflowId}/export` |
| Import workflow | `POST .../workflows/import` |
| Bulk cancel | `POST .../workflows/bulk-cancel` |
| Bulk resume | `POST .../workflows/bulk-resume` |
| Bulk delete | `POST .../workflows/bulk-delete` |
| Bulk fork from failure | `POST .../workflows/bulk-fork-from-failure` |
The semantics of cancelling, resuming, and forking are described in [Workflow Management](../workflow-management.md). The bulk variants take an array of workflow IDs and apply the same operation to each, which is far cheaper than issuing the calls one at a time. **Export** and **import** move a workflow and its steps between deployments as a JSON document — useful for reproducing a production failure in a development environment.
#### Queues
| Operation | Endpoint |
| --- | --- |
| List queues | `GET /v2/orgs/{orgName}/apps/{appName}/queues` |
| Get queue | `GET /v2/orgs/{orgName}/apps/{appName}/queues/{queueName}` |
#### Autoscaling
| Operation | Endpoint |
| --- | --- |
| Get autoscaling policy | `GET /v2/orgs/{orgName}/apps/{appName}/autoscaling-policy` |
| Set autoscaling policy | `PUT /v2/orgs/{orgName}/apps/{appName}/autoscaling-policy` |
| Delete autoscaling policy | `DELETE /v2/orgs/{orgName}/apps/{appName}/autoscaling-policy` |
| Desired executors, all versions | `GET /v2/orgs/{orgName}/apps/{appName}/autoscale` |
| Desired executors, one version | `GET /v2/orgs/{orgName}/apps/{appName}/autoscale/versions/{version}` |
The policy names the queue whose backlog drives the executor count; the two `autoscale` operations return how many executors each application version needs right now. `{version}` is a registered version or `latest`. See [Autoscaling and Version Management](../autoscaling.md).
#### Schedules
| Operation | Endpoint |
| --- | --- |
| List schedules | `GET /v2/orgs/{orgName}/apps/{appName}/schedules` |
| Get schedule | `GET /v2/orgs/{orgName}/apps/{appName}/schedules/{scheduleName}` |
| Pause schedule | `POST .../schedules/{scheduleName}/pause` |
| Resume schedule | `POST .../schedules/{scheduleName}/resume` |
| Trigger schedule | `POST .../schedules/{scheduleName}/trigger` |
| Backfill schedule | `POST .../schedules/{scheduleName}/backfill` |
**Trigger** runs a scheduled workflow immediately, out of band, and returns the started workflow's ID. **Backfill** replays a schedule across a past time window, starting one workflow per occurrence the schedule would have fired, and returns all of their IDs.
#### Alerting and audit logs
| Operation | Endpoint |
| --- | --- |
| List alerting rules | `GET /v2/orgs/{orgName}/apps/{appName}/alerting-rules` |
| Create alerting rule | `POST /v2/orgs/{orgName}/apps/{appName}/alerting-rules` |
| Delete alerting rule | `DELETE /v2/orgs/{orgName}/apps/{appName}/alerting-rules/{ruleId}` |
| List audit logs | `GET /v2/orgs/{orgName}/audit-logs` |
Alerting rules are described in [Alerting](../alerting.md). Audit log listing accepts `startTime`, `endTime`, `operation`, `subject`, and `target` filters alongside `limit` and `offset`; see [Audit Logs](../audit-logs.md).
### Self-Hosted Differences
A self-hosted Conductor can run with OIDC authentication enabled or with authentication disabled entirely (see the [Self-Hosting Guide](../self-hosting/hosting-conductor.md)). In no-auth mode there is no user identity and no multi-organization concept, so the operations that depend on them are **not registered at all** and respond `404`:
- every organization operation: `getOrg`, `updateOrg`, `joinOrg`, `generateSecret`, `listMembers`, `removeMember`, `listDomainClaims`, `requestDomainClaim`, and `releaseDomainClaim`;
- every role operation: `listRoles`, `createRole`, `deleteRole`, `grantRole`;
- the user operations `registerUser` and `getCurrentUser`;
- audit log listing, `listAuditLogs`.
These operations are marked in the spec with the `x-dbos-requires-oauth` extension, so a generated client or a tool reading the spec can identify them without hardcoding a list:
```shell
jq -r '.paths | to_entries[] | .key as $p | .value | to_entries[]
| select(.value["x-dbos-requires-oauth"])
| "\(.key | ascii_upcase) \($p)"' openapi.json
```
Everything else — applications, workflows, queues, schedules, alerting, and metrics — behaves identically in all three modes.
### Generating a Client
Because the spec is generated from the server rather than maintained by hand, generating your client from it is the recommended way to call the API. Any OpenAPI generator works. For example, with [`oapi-codegen`](https://github.com/oapi-codegen/oapi-codegen) for Go:
```shell
curl -o openapi.json https://cloud.dbos.dev/conductor/v2/openapi.json
go tool oapi-codegen -package conductor -generate client,types openapi.json > conductor.gen.go
```
Or with [`openapi-python-client`](https://github.com/openapi-generators/openapi-python-client):
```shell
curl -o openapi-3.0.json https://cloud.dbos.dev/conductor/v2/openapi-3.0.json
openapi-python-client generate --path openapi-3.0.json
```
Pin the generated client to a checked-in copy of the spec and regenerate deliberately, so that a change to the deployed API surfaces as a reviewable diff rather than as a silent change in your build.
If you would rather not write a client at all, the [`dbosctl` CLI](./dbosctl.md) already covers the operational surface of this API from the command line.
---
## Account Management
In this guide, you'll learn how to manage DBOS Cloud accounts.
#### New User Registration
You can sign up for an account on the [DBOS Cloud console](https://console.dbos.dev/login-redirect).
Additionally, all `dbos-cloud` commands prompt you to register a new account if you don't already have one.
#### Authenticating Programatically
Sometimes, such as in a CI/CD pipeline, it is useful to authenticate programatically without providing credentials through a browser-based login portal.
DBOS Cloud provides this capability with refresh tokens.
To obtain a refresh token, run:
```
dbos-cloud login --get-refresh-token
```
This command has you authenticate through the browser, but obtains a refresh token and stores it in `.dbos/credentials`.
Once you have your token, you can use it to authenticate programatically without going through the browser with the following command:
```
dbos-cloud login --with-refresh-token
```
Refresh tokens automatically expire after a year or after a month of inactivity.
You can manually revoke them at any time:
```
dbos-cloud revoke
```
:::warning
Until they expire or are revoked, refresh tokens can be used to log in to your account.
Treat them as secrets and keep them safe!
:::
#### Organization Management
:::info
This feature is currently only available to [DBOS Pro or Enterprise](https://www.dbos.dev/pricing) subscribers.
:::
Organizations allow multiple users to collaboratively manage applications.
When a user creates an account, they are automatically added to an organization containing only them, where the organization name is the same as their username.
You can manage your organization from the [cloud console organizations page](https://console.dbos.dev/settings/organization):

##### Organization Admins
The original creator of an organization is the organization admin.
Only the organization admin can invite new users, delete existing users, or rename the organization.
All users have full access to organization resources, including databases and applications.
##### Inviting New Users
To invite a new user to your organization, click the "Generate Invite Link" button.
This generates a **single-use** URL for joining your organization.
When a user signs in to the cloud console using that URL, they are prompted to join your organization.
If they do not have an account, they are prompted to create one.
If they already have an account, they must delete all resources (applications and databases) before joining your organization.
##### Renaming Your Organization
You can rename your organization by clicking the icon next to your organization name.
Note that applications belonging to organizations are hosted at the URL `https://-.cloud.dbos.dev/`.
**Therefore, renaming your organization changes your application URLs**.
##### Removing Users
The organization admin can remove any other user from their organization.
This immediately terminates their access to all organization resources.
---
## Application Management
#### Deploying Applications
To deploy your application to DBOS Cloud or update an existing application, run this command in its root directory:
```shell
dbos-cloud app deploy
```
Each time you deploy an application, the following steps execute:
1. **Upload**: An archive of your application folder is created and uploaded to DBOS Cloud. This archive can be up to 500 MB in size.
2. **Configuration**: Your application's dependencies [are installed](#dependency-management).
3. **Migration**: If you specify database migrations in your `dbos-config.yaml`, these are run on your cloud database.
4. **Deployment**: Your application is deployed to a number of [Firecracker microVMs](https://firecracker-microvm.github.io/) also referred to as "executors."
By default, these have 1 vCPU and 512MB of RAM.
The amount of memory allocated to each microVM is [configurable](./cloud-cli.md#dbos-cloud-app-update).
After an application is deployed, it is assigned a domain of the form `https://-.cloud.dbos.dev/`.
If your account is part of an [organization](./account-management.md#organization-management), organization name is used instead of username.
:::tip
* Applications should serve requests from port 8000 (Python—the default port for FastAPI and Gunicorn) or 3000 (TypeScript—the default port for Express and Koa).
* Multiple applications can connect to the same Postgres database server—they are deployed to isolated databases on that server.
* To change the database server of a deployed application, use `dbos-cloud app change-database-instance`.
:::
##### Applications Configuration
You need to provide a valid `dbos-config.yaml` file when deploying an application to DBOS Cloud. The required fields are:
- **name**: Your application name. This is the name with which your application is registered.
- **language**: `node` or `python`
- **runtimeConfig.start**: the command used to start your application. For example, `fastapi run`
Note that some fields from dbos-config.yaml will be **ignored** during cloud deployments:
- **database_url** and **database** connection-related fields. DBOS Cloud automatically applies the connection information of your cloud database server.
##### Dependency Management
**Python**
For Python applications, DBOS Cloud installs all dependencies from your `requirements.txt` file.
The maximum size of your application after all dependencies are installed is 2 GB.
**TypeScript**
For TypeScript applications, DBOS Cloud installs all dependencies from your `package-lock.json` file (or from `package.json` if no lockfile is provided).
The maximum size of your application after all dependencies are installed is 2 GB.
After all dependencies are installed, your application is compiled using `npm run build`.
##### Customizing MicroVM Setup
DBOS Pro subscribers can provide a _setup script_ that runs before their application is configured.
This script can customize the runtime environment for your application, for example installing system packages and libraries.
A setup script must be specified in your `dbos-config.yaml` like so:
```yaml title="dbos-config.yaml"
runtimeConfig:
# Script DBOS Cloud runs to customize your application runtime.
# Requires a DBOS Pro subscription.
setup:
- "./build.sh"
# Command DBOS Cloud executes to start your application.
start:
```
A setup script may install system packages or libraries or otherwise customize the microVM image. For example:
```shell title="build.sh"
#!/bin/bash
# Install the traceroute package for use in your application
apt install traceroute
```
##### Ignoring files with .dbosignore
A `.dbosignore` file at the root of your project instructs the DBOS Cloud CLI to exclude resources from application deployment.
The syntax for this file is similar to `.gitignore`:
- Patterns are compatible with the [fast-glob library](https://www.npmjs.com/package/fast-glob)
- Lines ending with `/` are transformed into a recursive ignore `/**` to exclude everything within a directory.
- Lines starting with `#` are ignored.
- Some patterns are automatically excluded:
```shell
**/.dbos/**
**/node_modules/**
**/dist/**
**/.git/**
**/dbos-config.yaml
**/venv/**
**/.venv/**
**/.python-version
```
#### Monitoring and Debugging Applications
Here are some useful tools to monitor and debug applications:
- The [cloud console](https://console.dbos.dev) provides a web UI for viewing your applications and their traces and logs.
- To retrieve the last `N` seconds of your application's logs, run [`dbos-cloud app logs -l `](./cloud-cli.md#dbos-cloud-app-logs). Note that new log entries take a few seconds to appear.
- To retrieve the status of a particular application, run [`dbos-cloud app status `](./cloud-cli.md#dbos-cloud-app-status). To list all applications, run [`dbos-cloud app list`](./cloud-cli.md#dbos-cloud-app-list).
#### Managing Application Versions
Each time you deploy an application, it creates a new version with a unique ID.
You can view all previous versions of your application from the [cloud console](https://console.dbos.dev) or list them by running:
```
dbos-cloud app versions
```
You can redeploy a previous version of your application by passing `--previous-version ` to the [`app deploy`](./cloud-cli.md#dbos-cloud-app-deploy) command.
```shell
dbos-cloud app deploy --previous-version
```
#### MicroVM Termination
DBOS Cloud may, from time to time, stop your microVMs due to a variety of reasons, including app upgrade or scaling down (see below). When a microVM is stopped, DBOS Cloud performs the following steps:
1. Stops routing new HTTP traffic to the VM.
2. Sends SIGTERM to the `dbos` process (launched by your start command) and waits up to 10 seconds for it to exit.
3. If the process is still running, terminates it forcefully.
Any `PENDING` workflows run by the microVM are then recovered on another VM. See below.
#### Workflow Recovery
When a microVM running in DBOS Cloud stops (either due to a process crash or when scaling down), all of its workflows are automatically recovered to another microVM running the same application version. If no other microVM of that application version exists, DBOS Cloud launches a new one and instructs it to recover the workflows.
When you deploy a new version of your application, DBOS Cloud routes all requests and scheduled workflows to microVMs of the new application version.
Then, DBOS Cloud attempts to decommission microVMs running the previous application version.
If there are still `PENDING` or `ENQUEUED` workflows of that code version, DBOS Cloud leaves some number of microVMs alive to process those workflows until all are complete.
Periodically, DBOS Cloud checks if there are any `PENDING` or `ENQUEUED` workflows not assigned to any microVM.
If any are found, DBOS Cloud recovers them to a microVM of the appropriate application version (starting one if necessary).
#### Automatic and Manual Scaling
Accounts on the free 30-day trial are limited to 1 microVM per app. Apps for Pro accounts are auto-scaled. Auto-scaling occurs based on CPU or queue utilization.
For the latter, the queue must have `worker_concurrency` (or `workerConcurrency`) set. DBOS Cloud computes the current parallel task capacity - the number of tasks that can be executed simultaneously - using the product of worker_concurrency and the current number of microVMs: `capacity = worker_concurrency * num_microvms`.
Scaling up occurs when:
- the average microVM CPU utilization exceeds 85% for several seconds, or
- the number of enqueued tasks exceeds capacity for at least one of the queues.
Inversely, the app scales down when the average CPU utilization drops below 40% and the number of enqueued tasks drops below capacity for all queues.
To alter the auto-scaling behavior, you can manually set `min-executors` and/or `max-executors` using `app update` (see below).
#### Updating Applications
To change your executor RAM or the default number of microVMs, run:
```shell
dbos-cloud app update [options]
```
See the [DBOS Cloud CLI reference](./cloud-cli.md#dbos-cloud-app-update) for a list of properties you can update. Note that `app update` does not trigger a redeploy of the code, which you can do with the [`app deploy`](./cloud-cli.md#dbos-cloud-app-deploy) command.
#### Deleting Applications
To delete an application, run:
```shell
dbos-cloud app delete
```
You can also drop the application database with the `--dropdb` argument.
As each application has its own isolated database, this does not affect your other applications.
```shell
dbos-cloud app delete --dropdb
```
:::warning
This is a destructive operation and cannot be undone.
:::
---
## Bringing Your Own Database
In this guide, you'll learn how to bring your own Postgres database instance to DBOS Cloud and deploy your applications to it.
#### Linking Your Database to DBOS Cloud
To bring your own Postgres database instance to DBOS Cloud, you must first create a role DBOS Cloud can use to deploy and manage your apps.
By default this role must be named `dbosadmin` and must have the `LOGIN` and `CREATEDB` privileges:
```sql
CREATE ROLE dbosadmin WITH LOGIN CREATEDB PASSWORD '';
```
If you cannot use the name `dbosadmin`, you can specify a different role name when linking your database with the `--dbos-admin-name` flag.
Next, link your database instance to DBOS Cloud, entering the password for the admin role when prompted.
You must choose a database instance name that is 3 to 16 characters long and contains only lowercase letters, numbers and underscores.
```shell
dbos-cloud db link -H -p
```
You can now register and deploy applications with this database instance as normal! Check out our [applications management](./application-management.md) guide for details.
:::tip
DBOS Cloud is currently hosted in AWS us-east-1.
For maximum performance, we recommend linking a database instance hosted there.
:::
---
## CI/CD Best Practices
### Staging and Production Environments
To make it easy to test changes to your application without affecting your production users, we recommend using separate staging and production environments.
You can do this by deploying your application with different names for staging and production.
For example, when deploying `my-app` to staging, deploy using:
```shell
dbos-cloud app deploy my-app-staging
```
When deploying to production, use:
```shell
dbos-cloud app deploy my-app-prod
```
`my-app-staging` and `my-app-prod` are completely separate and isolated DBOS applications.
There's nothing special about the `-staging` and `-prod` suffixes—you can use any names you like.
:::info
If you manually specify the application database name by setting `app_db_name` in `dbos-config.yaml`, you must ensure each environment uses a different value of `app_db_name`.
:::
### Authentication
You should use [refresh tokens](./account-management#authenticating-programatically) to programmatically authenticate your CI/CD user with DBOS Cloud.
:::info
Upgrading to a DBOS Cloud paid plan will unlock [multi-user organizations](./account-management#organization-management) which you can use to setup dedicated users for CI/CD.
:::
---
## Cloud CLI Reference
### Installation
To globally install the DBOS Cloud CLI, run the following command:
```
npm install -g @dbos-inc/dbos-cloud@latest
```
### User Management Commands
#### `dbos-cloud register`
**Description:**
This command creates and registers a new DBOS Cloud account.
It provides a URL to a secure login portal you can use to create an account from your browser.
**Arguments:**
- `-u, --username `: Your DBOS Cloud username. Must be between 3 and 30 characters and contain only lowercase letters, numbers, and underscores (`_`).
- `-s, --secret [string]`: (Optional) An [organization secret](./account-management.md#organization-management) given to you by an organization admin. If supplied, adds your newly registered account to the organization.
:::info
If you register with an email and password, you also need to verify your email through a link we email you.
:::
---
#### `dbos-cloud login`
**Description:**
This command logs you in to your DBOS Cloud account.
It provides a URL to a secure login portal you can use to authenticate from your browser.
:::info
When you log in to DBOS Cloud from an application, a token with your login information is stored in the `.dbos/` directory in your application package root.
:::
---
#### `dbos-cloud logout`
**Description:**
This command logs you out of your DBOS Cloud account.
---
### Database Instance Management Commands
#### `dbos-cloud db provision`
**Description:**
This command provisions a Postgres database instance to which your applications can connect.
**Arguments:**
- ``: The name of the database instance to provision. Must be between 3 and 30 characters and contain only lowercase letters, numbers, underscores, and dashes.
- `-U, --username `: Your username for this database instance. Must be between 3 and 16 characters and contain only lowercase letters, numbers, and underscores.
- `-W, --password [string]`: Your password for this database instance. If not provided, will be prompted on the command line. Passwords must contain 8 or more characters.
---
#### `dbos-cloud db list`
**Description:**
This command lists all Postgres database instances provisioned by your account.
**Arguments:**
- `--json`: Emit JSON output
**Output:**
For each provisioned Postgres database instance, emit:
- `PostgresInstanceName`: The name of this database instance.
- `HostName`: The hostname of this database instance.
- `Port`: The connection port for this database instance.
- `Status`: The current status of this database instance (available or unavailable).
- `AdminUsername`: The administrator username for this database instance.
---
#### `dbos-cloud db status`
**Description:**
This command retrieves the status of a Postgres database instance
**Arguments:**
- ``: The name of the database instance whose status to retrieve.
- `--json`: Emit JSON output
**Output:**
- `PostgresInstanceName`: The name of the database instance.
- `HostName`: The hostname of the database instance.
- `Port`: The connection port for the database instance.
- `Status`: The current status of the database instance (available or unavailable).
- `AdminUsername`: The administrator username for the database instance.
---
#### `dbos-cloud db reset-password`
**Description:**
This command resets your password for a Postgres database instance.
**Arguments:**
- `[database-instance-name]`: The name of the database instance whose password to reset.
- `-W, --password [string]`: Your new password for this database instance. If not provided, will be prompted on the command line. Passwords must contain 8 or more characters.
---
#### `dbos-cloud db destroy`
**Description:**
This command destroys a previously-provisioned Postgres database instance.
**Arguments:**
- ``: The name of the database instance to destroy.
---
#### `dbos-cloud db url`
**Description:**
This command retrieves your cloud database connection URL.
**Arguments:**
- `[database-instance-name]`: The name of the database instance to which to connect.
- `-S, --show-password`: Whether to show your database password in the output.
- `-W, --password [string]`: Your password for this database instance. If not provided, will be prompted on the command line.
---
#### `dbos-cloud db link`
**Description:**
This command links your own Postgres database instance to DBOS Cloud.
Before running this command, please first follow our [tutorial](./byod-management) to set up your Postgres database.
:::info
This feature is currently only available to [DBOS Pro or Enterprise](https://www.dbos.dev/pricing) subscribers.
:::
**Arguments:**
- ``: The name of the database instance to link. Must be between 3 and 30 characters and contain only lowercase letters, numbers, underscores, and dashes.
- `-H, --hostname `: The hostname for your Postgres database instance (required).
- `-p, --port [number]`: The connection port for your Postgres database instance (default: `5432`).
- `-W, --password [string]`: The password for the `dbosadmin` role. If not provided, will be prompted on the command line. Passwords must contain 8 or more characters.
- `--dbos-admin-name `: Specify a custom Postgres role name for DBOS Cloud to administer the database as (default: `dbosadmin`).
---
#### `dbos-cloud db unlink`
**Description:**
This command unlinks a previously linked Postgres database instance.
**Arguments:**
- ``: The name of the database instance to unlink.
---
### Application Management Commands
#### `dbos-cloud app deploy`
**Description:**
This command must be run from an application root directory.
It executes the migration commands declared in `dbos-config.yaml`, deploys the application to DBOS Cloud (or updates its code if already deployed), and emits the URL at which the application is hosted, which is `https://-.cloud.dbos.dev/`.
**Arguments:**
- `[application-name]`: The name of the application to deploy. By default we obtain the application name from `dbos-config.yaml`. This argument overrides the package name.
- `-d, --database `: The name of the Postgres database instance to which this application will connect. This may only be set the first time an application is deployed and cannot be changed afterwards.
- `--verbose`: Logs debug information about the deployment process, including config file processing and files sent.
- `--configFile`: DBOS config file path (default: `dbos-config.yaml`).
- `-p, --previous-version `: The ID of a previous version of this application. If this is supplied, redeploy that version instead of deploying from the application directory. This will fail if the previous and current versions have different database schemas. You can list previous versions and their IDs with the [versions command](#dbos-cloud-app-versions).
---
#### `dbos-cloud app update`
**Description:**
Update an application metadata in DBOS Cloud. Increasing RAM or adjusting autoscaling configuration requires a DBOS Pro subscription.
**Arguments:**
- `[application-name]`: The name of the application to update.
- `--executors-memory-mib`: The amount of RAM, in MiB, to allocate to the application's executors. This value must be between 512 and 5120. Additional RAM is [billed](https://www.dbos.dev/dbos-pricing).
- `--min-executors `: The minimum number of microVMs to be allocated to this application. Acts as a floor for autoscaling.
- `--max-executors `: The maximum number of microVMs to be allocated to this application. The app won't auto-scale to a larger number.
:::info
This command does not trigger a redeployment of the application. To apply changes affecting the application's executors, you must redeploy the application with [`dbos-cloud app deploy`](#dbos-cloud-app-deploy).
:::
---
#### `dbos-cloud app delete`
**Arguments:**
- `[application-name]`: The name of the application to delete.
- `--dropdb`: Drop the application's database during deletion.
**Description:**
Delete an application from DBOS Cloud.
If run in an application root directory with no application name provided, delete the local application.
By default, this command does not drop your application's database. You can use the `--dropdb` parameter to drop your application's database (not the Postgres instance) and delete all application data.
To destroy the previously-provisioned Postgres instance, please use [`dbos-cloud db destroy`](#dbos-cloud-db-destroy).
---
#### `dbos-cloud app list`
**Description:**
List all applications you have registered with DBOS Cloud.
**Arguments:**
- `--json`: Emit JSON output
**Output:**
For each registered application, emit:
- `Name`: The name of this application
- `ID`: The unique ID DBOS Cloud assigns to this application.
- `PostgresInstanceName`: The Postgres database instance to which this application is connected.
- `ApplicationDatabaseName`: The database on this instance on which this application stores data.
- `Status`: The current status of this application (available or unavailable).
- `Version`: The currently deployed version of this application.
- `AppURL`: The URL at which the application is hosted.
---
#### `dbos-cloud app status`
**Arguments:**
- `[application-name]`: The name of the application to retrieve.
**Description:**
Retrieve an application's status.
If run in an application root directory with no application name provided, retrieve the local application's status.
**Arguments:**
- `--json`: Emit JSON output
**Output:**
- `Name`: The name of this application
- `ID`: The unique ID DBOS Cloud assigns to this application.
- `PostgresInstanceName`: The Postgres database instance to which this application is connected.
- `ApplicationDatabaseName`: The database on this instance on which this application stores data.
- `Status`: The current status of this application (available or unavailable).
- `Version`: The currently deployed version of this application.
- `AppURL`: The URL at which the application is hosted.
---
#### `dbos-cloud app versions`
**Arguments:**
- `[application-name]`: The name of the application to retrieve.
**Description:**
Retrieve a list of an application's past versions.
A new version is created each time an application is deployed.
If run in an application root directory with no application name provided, retrieve versions of the local application.
**Arguments:**
- `--json`: Emit JSON output
**Output:**
For each previous version of this application, emit:
- `ApplicationName`: The name of this application.
- `Version`: The ID of this version.
- `CreationTime`: The timestamp (in UTC with [RFC3339](https://datatracker.ietf.org/doc/html/rfc3339) format) at which this version was created.
---
#### `dbos-cloud app logs`
**Description:**
Retrieve an application's logs.
**Arguments:**
- `[application-name]`: The name of the application.
- `-l, --last `: How far back to query, in seconds from current time. Default is 3600 (one hour).
---
#### `dbos-cloud app cmd`
**Description:**
A debugging utility that lets you run a shell command on one of your app's executors. Prints the `stderr` and `stdout` output by the command. The command must finish in 10 seconds. Every command is also recorded, without its output, in the app logs at `WARN` level. Note that stopping the running `dbos` process destroys the executor and causes it to be replaced by a new one.
**Arguments:**
- `-e, --executor-id `: The ID of the executor to use (see app logs).
- `-c, --command `: The shell command to run.
---
#### `dbos-cloud app resource-usage`
**Description:**
Retrieve your applications' resource usage for a specific time interval. If no time range is provided, queries for a recent completed 1-minute interval of data.
**Arguments:**
- `-s, --since `: UTC time since which to start querying (formatted as 2006-01-02 15:04:05.000000). Defaults to the start of a 1-minute interval ~2 minutes ago.
- `-u --upto `: UTC time up to which to start querying (formatted as 2006-01-02 15:04:05.000000). Defaults to the end of a 1-minute interval ~2 minutes ago.
- `-g, --group-by `: Time interval for grouping data: 'minute', 'hour', or 'day', defaults to 'minute'.
---
#### `dbos-cloud app change-database-instance`
**Description:**
This command must be run from an application root directory.
It redeploys the application to a new database instance.
**Arguments:**
- `--verbose`: Logs debug information about the deployment process, including config file processing and files sent.
- `-d, --database ` The name of the new database instance for this application.
- `-p, --previous-version `: The ID of a previous version of this application. If this is supplied, redeploy that version instead of deploying from the application directory.
---
#### `dbos-cloud app secrets create`
**Description:**
Create a new secret associated with an application, or update an existing secret.
Secrets are made available to your application as environment variables.
You must redeploy your application for a change in its secrets to take effect.
**Arguments:**
- `[application-name]`: The name of the application for which to create or update secrets.
- `-s, --name ` The name of the secret to create or update.
- `-v, --value`: The value of the secret.
---
#### `dbos-cloud app secrets import`
**Description:**
Import all environment variables defined in a `.env` file as secrets, updating them if they already exist.
Allowed syntax for the `.env` file is described [here](https://dotenvx.com/docs/env-file), note that interpolation is supported but command substitution and encryption are currently not.
Secrets are made available to your application as environment variables.
You must redeploy your application for a change in its secrets to take effect.
**Arguments:**
- `[application-name]`: The name of the application for which to import secrets.
- `-d, --dotenv ` Path to the `.env` file to import.
---
#### `dbos-cloud app secrets list`
**Description:**
List all secrets associated with an application (only their names, not their values).
**Arguments:**
- `[application-name]`: The name of the application for which to list secrets.
---
#### `dbos-cloud app secrets delete`
**Description:**
Delete a secret associated with an application.
:::warning
This action is irreversible
:::
**Arguments:**
- `[application-name]`: The name of the application for which to list secrets.
- `-s, --name ` The name of the secret to delete.
---
### Organization Management Commands
#### `dbos-cloud org list`
**Description:**
List users in your organization
**Arguments:**
- `--json`: Emit JSON output
---
#### `dbos-cloud org invite`
**Description:**
Generate an organization secret with which to invite another user into your organization. Organization secrets are single-use and expire after 24 hours.
**Arguments:**
- `--json`: Emit JSON output
---
#### `dbos-cloud org join`
**Description:**
Join your account to an organization. This gives you full access to the organization's resources.
**Arguments:**
- ``: The name of the organization you intend to join.
- ``: An organization secret given to you by an organization admin.
---
#### `dbos-cloud org rename`
**Description:**
Rename your organization. Only the organization admin (the original creator of the organization) can run this command. After running this command, [log out](#dbos-cloud-logout) and [log back in](#dbos-cloud-login) to refresh your local context.
**Arguments:**
- ``: The current name of your organization.
- ``: The new name for your organization.
:::info
Applications belonging to organizations are hosted at the URL `https://-.cloud.dbos.dev/`, so renaming your organization changes your application URLs. The old URLs are no longer accessible.
:::
---
#### `dbos-cloud org remove`
**Description:**
Remove a user from an organization. Only the organization admin (the original creator of the organization) can run this command.
**Arguments:**
- ``: The user to remove from your organization.
---
### Workflow Management Commands
#### `dbos-cloud workflow list`
**Description:**
List workflows run by your application in JSON format ordered by recency (most recently started workflows last).
**Arguments:**
- `[application-name]`: The name of your application
- `-l, --limit ` Limit the results returned (default: "10")
- `-o, --offset ` Skip workflows from the results returned.
- `-u, --workflowUUIDs ` Retrieve specific UUIDs
- `-U, --user ` Retrieve workflows run by this user
- `-s, --start-time ` Retrieve workflows starting after this timestamp (ISO 8601 format)
- `-e, --end-time ` Retrieve workflows starting before this timestamp (ISO 8601 format)
- `-S, --status ` Retrieve workflows with this status (`PENDING`, `SUCCESS`, `ERROR`, `MAX_RECOVERY_ATTEMPTS_EXCEEDED`, `ENQUEUED`, `DELAYED`, or `CANCELLED`)
- `-v, --application-version ` Retrieve workflows with this application version
- `-n, --name ` Retrieve functions with this name
---
#### `dbos-cloud workflow queue list`
**Description:**
Lists all currently enqueued functions in JSON format ordered by recency (most recently enqueued functions last).
**Arguments:**
- `[application-name]`: The name of your application
- `-n, --name ` Retrieve functions with this name
- `-s, --start-time ` Retrieve functions starting after this timestamp (ISO 8601 format)
- `-e, --end-time ` Retrieve functions starting before this timestamp (ISO 8601 format)
- `-S, --status ` Retrieve workflows with this status (`PENDING`, `SUCCESS`, `ERROR`, `MAX_RECOVERY_ATTEMPTS_EXCEEDED`, `ENQUEUED`, `DELAYED`, or `CANCELLED`)
- `-l, --limit ` Limit the results returned
- `-o, --offset ` Skip functions from the results returned (for pagination)
- `-q, --queue ` Retrieve functions run on this queue
#### `dbos-cloud workflow cancel`
**Description:**
Cancel a workflow, setting its status to `CANCELLED`.
If the workflow is currently executing, cancelling it preempts its execution (interrupting it at the beginning of its next step).
If the workflow is enqueued, cancelling removes it from the queue.
**Arguments:**
- `[application-name]`: The name of your application
- `-w, --workflowid`: The ID of the workflow to cancel.
#### `dbos-cloud workflow resume`
**Description:**
Resume a workflow from its last completed step.
You can use this to resume workflows that are cancelled or that have exceeded their maximum recovery attempts.
You can also use this to start an `ENQUEUED` workflow, bypassing its queue.
**Arguments:**
- `[application-name]`: The name of your application
- `-w, --workflowid`: The ID of the workflow to resume.
---
## Database Management
#### Provisioning Database Instances
Before you can deploy an application to DBOS Cloud, you must provision a Postgres database instance (server) for it.
You must choose a database instance name, username and password.
:::info
* Both the database instance name and username must be 3 to 16 characters long and contain only lowercase letters, numbers and underscores.
* The username must start with a letter.
* The usernames `dbosadmin`, `dbos`, `postgres` and `admin` are reserved and cannot be used.
* The database password must contain between 8 and 128 characters, and cannot contain the characters `/`, `"`, `@`, `'`, or whitespaces.
:::
Run this command and choose your database password when prompted:
```shell
dbos-cloud db provision -U
```
:::info
A Postgres database instance (server) can host many independent databases used by different applications.
Each application is deployed to an isolated database by default; you can configure this through the `app_db_name` field in `dbos-config.yaml`.
:::
:::info
If you forget your database password, you can always [reset it](./cloud-cli.md#dbos-cloud-db-reset-password).
:::
To see a list of all provisioned instances and their statuses, run:
```shell
dbos-cloud db list
```
To retrieve the status of a particular instance, run:
```shell
dbos-cloud db status
```
#### Database Schema Management
Every time you deploy an application to DBOS Cloud, it runs all migrations defined in your `dbos-config.yaml`.
Sometimes, it may be necessary to manually perform schema changes on a cloud database, for example to recover from a schema migration failure.
To make this easier, you can retrieve your cloud database connection URL by running:
```shell
dbos-cloud db url
```
You can then use it to run locally any migration command (for example, a down-migration command in your schema migration tool) and it will execute on your cloud database.
:::warning
While it is occasionally necessary, be careful when manually changing the schema on a production database.
:::
:::warning
Be careful making breaking schema changes such as deleting or renaming a column—they may break active workflows running on a previous application version.
:::
#### Destroying Database Instances
To destroy a database instance, run:
```shell
dbos-cloud db destroy
```
:::warning
Take care—this will irreversibly delete all data in the database instance.
:::
---
## Deploying to DBOS Cloud
:::info
To use DBOS Cloud, please [contact sales](https://dbos.dev/contact).
:::
Any application built with DBOS can be deployed to DBOS Cloud.
DBOS Cloud is a serverless platform for durably executed applications.
It provides:
- [**Application hosting and autoscaling**](./application-management.md): Managed hosting of your application in the cloud, automatically scaling to millions of users. Applications are charged only for the CPU time they actually consume.
- [**Managed workflow recovery**](./application-management.md): If a cloud executor is interrupted, crashed, or restarted, each of its workflows is automatically recovered by another executor.
- [**Workflow and queue management**](./workflow-management.md): Dashboards of all active and past workflows and all queued tasks, including their status, inputs, outputs, and steps. Cancel, resume, or fork any workflow execution and manage the tasks in your distributed queues.
### Deploying Your App to DBOS Cloud
##### 1. Install the DBOS Cloud CLI
The Cloud CLI requires Node.js 20 or later.
Instructions to install Node.js
**macOS or Linux**
Run the following commands in your terminal:
```bash
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
nvm install 22
nvm use 22
```
**Windows**
Download Node.js 20 or later from the [official Node.js download page](https://nodejs.org/en/download) and install it.
After installing Node.js, create the following folder: `C:\Users\%user%\AppData\Roaming\npm`
(`%user%` is the Windows user on which you are logged in).
Run this command to install it.
```shell
npm i -g @dbos-inc/dbos-cloud@latest
```
##### 2. Create a requirements.txt File
Create a `requirements.txt` file listing your application's dependencies.
As DBOS Cloud uses OpenTelemetry to export your application's logs and traces, you should install the DBOS OpenTelemetry dependencies through `pip install dbos[otel]` before generating `requirements.txt`.
```shell
pip freeze > requirements.txt
```
##### 3. Define a Start Command
Set the `start` command in the `runtimeConfig` section of your [`dbos-config.yaml`](../../../python/reference/configuration.md) to your application's launch command.
If your application includes an HTTP server, configure it to listen on port 8000.
To test that it works, try launching your application with `dbos start`.
```yaml
runtimeConfig:
start:
- "fastapi run"
```
##### 4. Deploy to DBOS Cloud
Run this single command to deploy your application to DBOS Cloud!
```shell
dbos-cloud app deploy
```
##### 1. Install the DBOS Cloud CLI
Run this command to install the Cloud CLI globally.
```shell
npm i -g @dbos-inc/dbos-cloud@latest
```
As DBOS Cloud uses OpenTelemetry to export your application's logs and traces, you must install the DBOS OpenTelemetry dependencies into your application:
```shell
npm i @dbos-inc/otel@latest
```
##### 2. Define a Start Command
Set the `start` command in the `runtimeConfig` section of your [`dbos-config.yaml`](../../../typescript/reference/configuration.md) to your application's launch command.
If your application includes an HTTP server, configure it to listen on port 3000.
To test that it works, try launching your application with `npx dbos start`.
```yaml
runtimeConfig:
start:
- "npm start"
```
##### 3. Deploy to DBOS Cloud
Run this single command to deploy your application to DBOS Cloud!
```shell
dbos-cloud app deploy
```
##### 1. Install the DBOS Cloud CLI
The Cloud CLI requires Node.js 20 or later.
Instructions to install Node.js
**macOS or Linux**
Run the following commands in your terminal:
```bash
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
nvm install 22
nvm use 22
```
**Windows**
Download Node.js 20 or later from the [official Node.js download page](https://nodejs.org/en/download) and install it.
After installing Node.js, create the following folder: `C:\Users\%user%\AppData\Roaming\npm`
(`%user%` is the Windows user on which you are logged in).
Run this command to install it.
```shell
npm i -g @dbos-inc/dbos-cloud@latest
```
##### 2. Configure DBOS
Your DBOSContext [Config](../../../golang/reference/dbos-context.md) must be set with:
- `DatabaseURL` (or your custom `pgxpool`) must point to an environment variable named `DBOS_SYSTEM_DATABASE_URL`
```go
dbos.Config{
AppName: "dbos-starter",
ApplicationVersion: "0.1.0",
DatabaseURL: os.Getenv("DBOS_SYSTEM_DATABASE_URL"),
}
```
##### 3. Provide a configuration file
In a file named `dbos-config.yaml`, set these fields:
- `name`: your application name. Must match `AppName` in your `dbos.Config`
- `language`: must be `go`
```yaml
name: your-app-name
language: go
```
##### 4. Deploy to DBOS Cloud
Finally, build your application under the name `main`, against linux/amd64, then run this command to deploy your application to DBOS Cloud!
:::info
DBOS Cloud will serve HTTP traffic on port 8080. Make sure to use that port when configuring web servers.
:::
```shell
GOOS=linux GOARCH=amd64 go build -o main .
dbos-cloud app deploy
```
### DBOS Cloud How-Tos
#### HTTP Serving & Port Numbers
DBOS Cloud provides your application with an HTTPS URL and routes traffic to it.
It expects applications to listen for HTTP requests on port 3000 (TypeScript), port 8000 (Python), or port 8080 (Go and Java).
#### Environment Management
We recommend you configure your application with environment variables.
You can use [DBOS Cloud secrets](./secrets.md) to pass environment variables to your cloud application.
You can even [import secrets](./secrets.md#importing-secrets) into DBOS Cloud from a `.env` file.
#### Database Setup
##### System Database
DBOS Cloud automatically constructs a system database for your application.
You can access it at `DBOS_SYSTEM_DATABASE_URL`.
You should not attempt to override or modify this URL.
##### Cloud-Provided Application Databases
You may connect an application database to your application through DBOS Cloud.
If you do, connection information is provided via the `DBOS_DATABASE_URL` environment variable.
You may optionally direct DBOS Cloud to run migrations to set up your application's database schema by specifying migration commands in your `dbos-config.yaml` file:
```yaml
migrate:
- npx knex migrate:latest
```
DBOS Cloud will provide a database URL to your migrations through the `DBOS_DATABASE_URL` environment variable.
---
## Monitoring Your Applications
The [DBOS Cloud Console](https://console.dbos.dev) provides several tools to monitor your applications.
#### Logs
You can view your application's logs from your application's cloud console page.
Logs are paginated and ordered chronologically.

#### Traces
You can view traces for all your applications from the cloud console [traces page](https://console.dbos.dev/traces).
You can filter traces by application, time, operation, type, and status.
Traces are sorted chronologically and displayed hierarchically.
You can click on a trace or span to see detailed information about it.

#### Grafana Dashboard
You can launch a Grafana dashboard for DBOS Cloud from the cloud console [dashboard page](https://console.dbos.dev/dashboard).
##### Time Selection
In the top-right corner, the Grafana dashboard provides a time selector, defaulting to the last hour. You can change this setting to navigate to a different window of time. All of the panels are filtered for the selected time interval.

Under the time selector you can find time series for active CPU milliseconds used by your apps and the counts of logs and traces, summarized for every minute. Counts of warnings, errors and fatal errors are color coded as yellow, red and purple respectively. These panes have a matched time axis. You can click and drag across an interesting region in the series to "zoom in".

Zooming in will update the time selector and, therefore, all other panels. You can then use the time selector to "zoom back out" or use the `<` and `>` buttons to move backwards and forwards in time.
##### Grafana Logs and Traces
On the left side, the Grafana dashboard provides a log view with entries generated by your applications arranged chronologically. The pane displays up to 1,000 most recent log records in the selected time period. Special entries for application lifetime events are colored grey and labeled as `[APP REGISTER]`, `[APP DEPLOY]` and so on. These are generated by DBOS Cloud automatically and not shown in the summarized log counts. You can click on a log record to browse additional metadata. In the example below, we see logs for an example app first getting registered, then undergoing schema migration, then getting deployed:

Under the logs pane, there is an expandable panel of traces. Each row corresponds to a handler, workflow, transaction or step span. Each span is timestamped and decorated with duration in milliseconds, the IDs of the trace and workflow it belongs to, its execution status, and other information.
##### Filtering
In the top-right corner of the Grafana dashboard, there are filtering selectors:

1. you can select a single `Application Name` to filter for. Refresh the browser to update the list of names for a new app.
2. you can paste a specific `Trace ID` to only view logs and spans for that Trace. To clear, erase the text and press "return."
3. similar to Trace ID you can copy-paste a specific `Workflow UUID` to filter by that. It is cleared the same way as Trace ID.
4. you can select `Min Severity` to filter logs and traces for a specific severity level or higher
5. you can select `Application Version` to filter all data for a specific version of your app
6. you can select an `Executor ID` to only show data for a specific Micro VM
:::tip
When turning on these filters, the time window filter also still applies. You may see more data for your selection if you "zoom out" in time.
:::
When using `Workflow UUID` use `_` to match any one character and `%` to match any string (SQL 'like' notation). This is useful for selecting groups of scheduled workflows. For example you can use a string like `sched%T19%` to match any scheduled workflows that ran at 7PM on any of the days in the selected time interval. `Search` also supports this syntax.
##### RAM Time, Requests and CPU Milliseconds
The dashboard tracks the total RAM 512MB-Hours, Requests and active CPU Milliseconds for all your apps. These totals are updated every time you refresh your dashboard. They are applied against your DBOS Pricing tier's [execution time limit](https://www.dbos.dev/pricing). Please allow up to 5 minutes of delay between an event happening and the dashboard refresh showing it.
The number of total CPU milliseconds since the start of the month is in orange. The light orange "selection" number to the right of it changes with the selected app(s) and time window. You can select a particular app or workload and see how much it contributes to your total.

:::tip
It is possible for one or two small API calls to not consume a measurable amount of CPU ms. It is also normal for an idle app to use a negligible amount of CPU ms for periodic health checks and background tasks. For best results, run an example workflow of at least 10 API calls (the more the better). Observe how much CPU ms your example uses and extrapolate to your monthly expected usage.
:::
##### Memory and CPU Metrics
MicroVM Metrics is an expandable panel under the Traces panel. This shows the number of running executors, their RAM and CPU usage over time. These plots are filtered for time and the selected app. If you're running multiple VMs, the CPU % and RAM usage plots show a separate colored line for each selected VM.
##### Dashboards and Organizations
If you are part of a multi-user organization, your Grafana dashboard will show data for all applications deployed by all users in the organization. The log entries for application lifetime events (labeled as `[APP REGISTER]`, `[APP DEPLOY]` and so on) are annotated with the email address of the user performing each action.
---
## Export Logs and Traces
This tutorial shows how to configure your DBOS Cloud application to export OpenTelemetry logs and traces to a third party observability service. If your service accepts the OTEL format, you can skip steps 1 and 2. Simply pass environment variables like `OTEL_EXPORTER_OTLP_HEADERS` as [app secrets](./secrets.md) (see [step 3](#3-set-the-datadog-api-key-to-your-apps-environment)) and then configure logs and traces endpoints as shown in [step 4](#4-configure-your-app-to-export-logs-and-traces-to-otel-contrib).
Other services may require additional software. Here we use Datadog as an example. We connect by installing the otel-contrib package in the App VM at deployment time and configuring it with the Datadog API key to export data.
:::info
These steps require a [DBOS Pro or Enterprise](https://www.dbos.dev/pricing) subscription.
:::
### 1. Create a Custom VM Setup Script
In your app directory (next to `dbos-config.yaml`) create the following script called `build.sh`. Make sure to set its permissions to execute.
```bash
#!/bin/bash
# Download and install otel-contrib in the MicroVM
curl -L -O https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v0.121.0/otelcol-contrib_0.121.0_linux_amd64.deb
dpkg -i otelcol-contrib_0.121.0_linux_amd64.deb
rm otelcol-contrib_0.121.0_linux_amd64.deb
# Configure and enable it
cat < /etc/otelcol-contrib/config.yaml
receivers:
otlp:
protocols:
grpc:
http:
endpoint: "0.0.0.0:4318"
processors:
batch:
exporters:
datadog:
api:
site: datadoghq.com #this URL depends on your datadog region
key: ${DATADOG_API_KEY} #this is passed in a secret or env (see below)
service:
pipelines:
metrics:
receivers: [otlp]
processors: [batch]
exporters: [datadog]
traces:
receivers: [otlp]
processors: [batch]
exporters: [datadog]
logs:
receivers: [otlp]
processors: [batch]
exporters: [datadog]
EOF
systemctl restart otelcol-contrib
systemctl enable otelcol-contrib
```
### 2. Configure Your App to Run the Script on Deploy
Add the build.sh script as a custom setup to your runtimeConfig in your `dbos-config.yaml`. See [Customizing MicroVM Setup](./application-management#customizing-microvm-setup) for more info.
```yaml
runtimeConfig:
setup:
- "./build.sh"
start:
- npm run start #or your custom start command
```
### 3. Set the Datadog API Key to Your App's Environment
After registering your app, set the API key like so:
```bash
dbos-cloud app register -d
dbos-cloud app secrets create -s DATADOG_API_KEY -v 678... #your key value
```
The script we created in step 1 will read this value and pass it to `otel-contrib`.
### 4. Configure your App to Export Logs and Traces to otel-contrib
In the app code, when creating the `DBOS` object, pass in the Logs and Traces endpoints like so:
**Python**
```python
from dbos import DBOSConfig
config: DBOSConfig = {
"name": "your-app-name",
"application_version": "0.1.0",
"otlp_traces_endpoints": [ "http://0.0.0.0:4318/v1/traces" ], #match the config in step 1 above
"otlp_logs_endpoints": [ "http://0.0.0.0:4318/v1/logs" ]
}
DBOS(config=config)
```
**Typescript**
```typescript
DBOS.setConfig({
"name": "your-app-name",
"applicationVersion": "0.1.0",
"otlpTracesEndpoints": [ "http://0.0.0.0:4318/v1/traces" ],
"otlpLogsEndpoints": [ "http://0.0.0.0:4318/v1/logs" ]
});
await DBOS.launch();
```
### 5. Add RAM if Needed, and Deploy!
Depending on your app’s other memory usage, you may need to increase your RAM limit to make room for the otel-contrib process.
```bash
dbos-cloud app update --executors-memory-mib 1024
dbos-cloud app deploy
```
Within a few minutes of deploying you should see your logs appear in Datadog.
---
## Workflow Retention Policies
You can configure workflow history retention policies for your application from the Retention Policy page of the DBOS Console.
These settings let you configure how long workflow history is retained in your application's [system database](../../../explanations/system-tables.md).
This is useful for managing the database disk usage of workflow history.
Retention policies only delete the history of completed workflows (workflows with status `SUCCESS`, `ERROR`, `CANCELLED`, or `MAX_RECOVERY_ATTEMPTS_EXCEEDED`); workflows that are still running, enqueued, or delayed are never deleted.
Deleting a workflow's history also deletes its steps, inputs, outputs, messages, events, and streams.
#### Time Threshold
If a time threshold is set, workflow history is only retained for X hours after a workflow completes.
History of workflows that completed more than X hours ago is automatically deleted.
Time-based retention is disabled by default.
#### Rows Threshold
If the rows threshold is set, history is only retained for the X most recently completed workflows.
History of older completed workflows is automatically deleted.
By default, the rows threshold is set to 1M rows.
You can set both a rows threshold and a time threshold.
#### Global Timeout
If a global timeout is set, any workflow that has not completed X hours after it was created (started or enqueued) is automatically cancelled.
By default, the global timeout is disabled.
---
## Secrets and Environment Variables
We recommend using _secrets_ to securely manage your application's secrets and environment variables in DBOS Cloud.
Secrets are key-value pairs that are securely stored in DBOS Cloud and made available to your application as environment variables.
Redeploy your application for newly created or updated secrets to take effect.
### Managing and Using Secrets
You can create or update a secret using the Cloud CLI:
```
dbos-cloud app env create -s -v
```
:::info
A few secret names are reserved and cannot be used. These are `DBOS_DATABASE_URL` and `DBOS_APP_HOSTNAME`.
:::
For example, to create a secret named `API_KEY` with value `abc123`, run:
```
dbos-cloud app env create -s API_KEY -v abc123
```
When you next redeploy your application, its environment will be updated to contain the `API_KEY` environment variable with value `abc123`.
You can access it like any other environment variable:
**Python**
```python
key = os.environ['API_KEY'] # Value is abc123
```
**Typescript**
```typescript
const key = process.env.API_KEY; // Value is abc123
```
Additionally, you can manage your application's secrets from the secrets page of the [cloud console](https://console.dbos.dev).
### Importing Secrets
You can import the contents of a `.env` file as secrets.
Allowed syntax for the `.env` file is described [here](https://dotenvx.com/docs/env-file). Note that interpolation is supported but command substitution and encryption are currently not.
Import a `.env` file with the following command:
```shell
dbos-cloud app env import -d
```
For example:
```shell
dbos-cloud app env import -d .env
```
### Listing Secrets
You can list the names of your application's secrets with:
```shell
dbos-cloud app env list
```
### Deleting a Secret
You can delete an environment variable with:
```shell
dbos-cloud app env delete -s
```
---
## Workflow Management
### Viewing Workflows
Navigate to the workflows tab of your application's page on the DBOS Console to see a list of its workflows:
This includes **all** your application's workflows: those currently executing, those enqueued for execution, those that have completed successfully, and those that have failed.
You can filter by time, workflow ID, workflow name, and workflow status (for example, you can search for all failed workflow executions in the past day).
Click on a workflow to see details, including its input and output:
Click "Show Workflow Steps" to view the workflow's execution as a trace timeline (showing the workflow, its steps, and its child workflows and their steps).
For example, here is the trace of a workflow that processes multiple tasks concurrently by enqueueing child workflows:
You can manage individual workflows directly from the DBOS Console.
##### Cancelling Workflows
You can cancel any workflow that has not completed: `PENDING`, `ENQUEUED`, or `DELAYED`.
Cancelling a workflow sets its status to `CANCELLED`.
If the workflow is currently executing, cancelling it preempts its execution (interrupting it at the beginning of its next step).
If the workflow is enqueued or delayed, cancelling removes it from the queue.
##### Resuming Workflows
You can resume any `ENQUEUED`, `DELAYED`, `CANCELLED` or `MAX_RECOVERY_ATTEMPTS_EXCEEDED` workflow.
Resuming a workflow resumes its execution from its last completed step.
If the workflow is enqueued, this bypasses the queue to start it immediately.
##### Forking Workflows
You can start a new execution of a workflow by **forking** it from a specific step.
To do this, open the workflow steps view, select a particular step, and click "Fork".
When you fork a workflow, DBOS generates a new workflow with a new workflow ID, copies to that workflow the original workflow's inputs and all its steps up to the selected step, then begins executing the new workflow from the selected step.
Forking a workflow is useful for recovering from outages in downstream services (by forking from the step that failed after the outage is resolved) or for "patching" workflows that failed due to a bug in a previous application version (by forking from the bugged step to an application version on which the bug is fixed).
---
## dbosctl CLI Reference
`dbosctl` is a command-line client for the [Conductor API](./conductor-api.md). It manages workflows, queues, schedules, applications, and API keys against DBOS-hosted Conductor or a [self-hosted Conductor](../self-hosting/hosting-conductor.md), with the target selected by a named **profile**.
The [`dbosctl sysdb`](#system-database-commands) commands are the exception: they manage the Postgres [system database](../../explanations/system-tables.md) directly, so they take a database URL rather than a profile.
### Installation
The install script detects your platform, verifies the download against the release checksums, and installs `dbosctl` to the first writable of `/usr/local/bin`, `~/.local/bin`, or the current directory:
```shell
curl -sSfL https://raw.githubusercontent.com/dbos-inc/dbos-ctl/main/install.sh | sh
```
Set `VERSION` to pin a release, or `BIN_DIR` to choose where it lands:
```shell
curl -sSfL https://raw.githubusercontent.com/dbos-inc/dbos-ctl/main/install.sh \
| VERSION=v0.1.0 BIN_DIR=~/.local/bin sh
```
You can also [download an archive directly](https://github.com/dbos-inc/dbos-ctl/releases).
Builds are published for Linux, macOS, and Windows on both amd64 and arm64, alongside a `checksums.txt`.
The binaries are statically linked, so they run on any Linux distribution, Alpine included.
If you already have a Go toolchain (1.24 or later), you can install from source instead:
```shell
go install github.com/dbos-inc/dbos-ctl/cmd/dbosctl@latest
```
However you install it, `dbosctl version` reports what you have — a downloaded release prints its tag, and a `go install` prints the module version it was built from.
### Quick Start
Against DBOS-hosted Conductor:
```shell
dbosctl config set managed --managed # create a profile pointing at cloud.dbos.dev
dbosctl login # log in through the device-authorization flow
dbosctl whoami # confirm who you are logged in as
dbosctl app list
```
Against a self-hosted Conductor with OIDC authentication, name the issuer and client
ID the deployment is configured with, then log in as you would against the managed
service:
```shell
dbosctl config set selfhosted --url https://conductor.example.com \
--issuer https://auth.example.com/realms/dbos --client-id dbos-cli
dbosctl login --profile selfhosted
dbosctl app list --profile selfhosted
```
Against a self-hosted Conductor running without authentication:
```shell
dbosctl config set local --url http://localhost:8090
dbosctl app list --profile local
```
### Profiles
A profile is a named bundle of connection settings: which Conductor to talk to, how to authenticate, and the default organization and application. Profiles are stored in `config.yaml` under your OS configuration directory — `~/.config/dbos/config.yaml` on Linux, `~/Library/Application Support/dbos/config.yaml` on macOS.
A profile must target either DBOS-hosted Conductor (`--managed`) or a self-hosted one (`--url`); the two are mutually exclusive. There are three common shapes:
| Shape | How to create it | Authentication | Identity |
| --- | --- | --- | --- |
| DBOS-hosted | `dbosctl config set --managed` | User JWT or `dbos_` API key | Your real user, or none for an API key |
| Self-hosted with OIDC | `dbosctl config set --url --issuer --client-id ` | User JWT or `dbos_` API key | Your real user, or none for an API key |
| Self-hosted, no auth | `dbosctl config set --url ` | None | Always `local` |
The two authenticated shapes accept the same credentials: Conductor tells a user JWT from an API key by the key's `dbos_` prefix, not by how it is deployed. What differs is where the OIDC settings come from — `--managed` derives them, self-hosted needs them spelled out.
`--managed` points the profile at `cloud.dbos.dev` and derives everything else — the `/conductor` base URL, bearer authentication, and the OIDC tenant — automatically.
Passing `--issuer`/`--client-id` implies bearer authentication, so `--auth` is only needed in the uncommon case of a self-hosted Conductor you reach with a `dbos_` API key but no OIDC login: pass `--auth bearer`. Because an API key carries no user identity, give that profile an `--org` as well.
### Authentication
```shell
dbosctl login # OIDC device flow against the profile's issuer; stores a token
dbosctl logout # discard the stored token for the current profile
```
`login` runs the [device-authorization flow](https://www.rfc-editor.org/rfc/rfc8628): it prints a URL and a code, you approve in a browser, and the resulting token is written to `credentials.json` (mode `0600`) next to `config.yaml`, keyed by profile. Tokens are refreshed automatically on expiry when the issuer returns a refresh token.
There are two ways to bypass the login flow:
- **`DBOS_TOKEN`** — a bearer token used as-is for a single invocation.
- **API keys** — a `dbos_…` key from [`dbosctl api-key create`](#dbosctl-api-key-create) or the console, supplied through `DBOS_TOKEN`. Keys authenticate machine-to-machine calls such as `app list`, but carry no user identity, so `dbosctl whoami` still requires a user login.
### Configuration Precedence
Each setting is resolved **flag → environment variable → profile**, so a flag always wins and the profile is the fallback:
| Setting | Flag | Environment variable |
| --- | --- | --- |
| Profile | `--profile` | `DBOS_PROFILE` |
| Conductor URL | `--url` | `DBOS_URL` |
| Organization | `--org` | `DBOS_ORG` |
| Application | `-a`, `--app` | `DBOS_APP` |
| Bearer token | — | `DBOS_TOKEN` |
| System database ([`sysdb`](#system-database-commands) only) | `-D`, `--db-url` | `DBOS_SYSTEM_DATABASE_URL` |
| Output format | `-o`, `--output` | — |
Flags are scoped to the command that uses them, so pass them **after** the command name (`dbosctl app list --org acme`). Each command's `--help` lists only the flags it honors.
### Common Flags
These flags are accepted by most commands and are not repeated in the reference below:
- `--profile `: Config profile to use. Overrides `$DBOS_PROFILE`.
- `--url `: Conductor base URL. Overrides `$DBOS_URL` and the profile.
- `--org `: Organization. Overrides `$DBOS_ORG` and the profile.
- `-a, --app `: Application name, for application-scoped commands. Overrides `$DBOS_APP` and the profile.
- `-o, --output `: Output format — `table` (default), `json`, or `ids`.
The [`dbosctl sysdb`](#system-database-commands) commands accept none of them except `-o`. They do not talk to Conductor, so there is no profile, organization, or application to name. `sysdb reset` and `sysdb rename-application` print row counts, so they honor `-o` like every other command that prints data — `table` or `json`, but not `ids`. (`sysdb reset` has an `--app` flag of its own, read from the command line only.)
### Output
Output is human-readable tables by default. `-o json` emits the raw API shape for scripting; it is never truncated or reprojected, so `dbosctl app list -o json` is exactly the JSON array the [Conductor API](./conductor-api.md) returned.
```shell
dbosctl app list # aligned table
dbosctl app list -o json # raw JSON array
dbosctl whoami -o json # raw user profile
```
Detail views — `workflow get`, `queue get`, and `schedule get` — include an `applicationName` row naming the application that owns the object, when it has one.
Commands with a natural identifier also accept `-o ids`, which prints one ID per line for piping. A literal `-` in place of arguments reads IDs from stdin:
```shell
dbosctl workflow list -a myapp --status PENDING -o ids | dbosctl workflow cancel -a myapp -
```
### Exit Codes
| Code | Meaning |
| --- | --- |
| `0` | Success |
| `1` | General error |
| `2` | Usage error (bad flags or arguments) |
| `3` | Authentication required (HTTP 401) — run `dbosctl login` |
| `4` | Not found (HTTP 404) |
| `130` | Interrupted (Ctrl-C) |
### Commands That Need a Running Application
Conductor answers some commands itself and forwards the rest to your application over the websocket its executors hold open, as described in [How Operations Are Served](./conductor-api.md#how-operations-are-served). The split follows the resource, not the verb, so it cuts across the reference below:
| | Commands |
| --- | --- |
| Forwarded to your application | All `workflow`, `queue`, and `schedule` commands — reads as much as mutations — plus `app versions` and `app set-version` |
| Answered by Conductor | Everything else except `sysdb`: the rest of `app`, plus `api-key`, `permission`, `login`, `logout`, `whoami`, and `config` |
| Answered by neither | The [`sysdb`](#system-database-commands) commands, which open the system database themselves |
Commands in the first group fail if the application has no healthy executor connected, so a failure there usually means your application is not running rather than that nothing matched.
### Authentication Commands
#### `dbosctl login`
**Description:**
Logs in to the current profile's Conductor using the OIDC device-authorization flow, storing the resulting token for later commands.
---
#### `dbosctl logout`
**Description:**
Removes the stored login for the current profile.
---
#### `dbosctl whoami`
**Description:**
Shows the logged-in identity. On a no-auth self-hosted target this always reports `local`. An API key carries no user identity, so this command requires a user login.
---
### Profile Commands
#### `dbosctl config list`
**Description:**
Lists all profiles, marking the current one.
---
#### `dbosctl config show`
**Description:**
Shows one profile's settings.
**Arguments:**
- `[profile]`: (Optional) The profile to show. Defaults to the current profile.
---
#### `dbosctl config use`
**Description:**
Sets the current profile, used by any command that does not pass `--profile`.
**Arguments:**
- ``: The profile to make current.
---
#### `dbosctl config set`
**Description:**
Creates or updates a profile. Only the flags you pass are changed; fields you do not name are left as they were.
**Arguments:**
- ``: The profile to create or update.
- `--managed`: Make this a DBOS-hosted Conductor profile (production domain `cloud.dbos.dev`). Mutually exclusive with `--url`.
- `--url `: Base URL of a self-hosted Conductor. Mutually exclusive with `--managed`.
- `--issuer `: OIDC issuer URL. Implies bearer authentication.
- `--client-id `: OIDC client ID. Implies bearer authentication.
- `--audience `: OIDC audience, for bearer profiles that require it.
- `--auth `: Force bearer authentication without OIDC, for a profile that authenticates with a `dbos_` API key only. The accepted value is `bearer`.
- `--org `: Organization. Only needed when it cannot be derived from a login, as with an API-key-only profile.
- `--app `: Default application for this profile.
---
### Application Commands
#### `dbosctl app list`
**Description:**
Lists the applications in the organization.
---
#### `dbosctl app get`
**Description:**
Shows one application's details.
**Arguments:**
- ``: The application's name.
---
#### `dbosctl app register`
**Description:**
Registers an application with Conductor. The name must match the application name in your DBOS configuration — see [Connecting To Conductor](../overview.md#connecting-to-conductor).
**Arguments:**
- ``: The application's name.
- `--private-mode`: Register the application in private mode, so it does not send workflow payload data — inputs, outputs, and events — to Conductor. Omit the flag to take Conductor's default; it can be changed later with [`dbosctl app update`](#dbosctl-app-update).
---
#### `dbosctl app update`
**Description:**
Updates an application's tuning settings. Only the flags you pass are changed.
**Arguments:**
- ``: The application's name.
- `--executor-timeout-secs `: Seconds before an idle executor is considered gone.
- `--global-timeout-ms `: Global workflow timeout, in milliseconds.
- `--gc-rows-threshold `: Number of most recently completed workflows whose history is kept; history of older completed workflows is garbage-collected. See [Workflow Retention Policies](../retention.md).
- `--gc-time-threshold-ms `: Time, in milliseconds, after a workflow completes before its history is garbage-collected.
- `--private-mode`: Whether the application is in private mode, in which it does not send workflow payload data — inputs, outputs, and events — to Conductor. Pass `--private-mode=false` to turn it back off.
---
#### `dbosctl app delete`
**Description:**
Deletes an application. Prompts for confirmation when run interactively.
**Arguments:**
- ``: The application's name.
- `--force`: Skip the confirmation prompt. Required when running non-interactively.
---
#### `dbosctl app versions`
**Description:**
Lists an application's versions.
**Arguments:**
- ``: The application's name.
---
#### `dbosctl app set-version`
**Description:**
Sets an application's latest version.
**Arguments:**
- ``: The application's name.
- ``: The version to mark as latest.
---
#### `dbosctl app executors`
**Description:**
Lists the executors currently connected to Conductor for an application.
**Arguments:**
- ``: The application's name.
---
#### `dbosctl app metrics`
**Description:**
Lists an application's metrics over a time window.
**Arguments:**
- ``: The application's name.
- `--since `: Report the window ending now and starting this long ago. Defaults to `24h`.
---
### Workflow Commands
Workflow commands are application-scoped: pass `-a/--app`, or set `DBOS_APP`, or give the profile a default application. The `wf` alias is accepted in place of `workflow`.
:::info
`resume` is stricter than every other command here: it needs an executor running the application's **latest** version, not merely a healthy one. A deployment that is connected but still rolling out can therefore resume nothing, while everything else works.
:::
#### `dbosctl workflow list`
**Description:**
Lists workflows matching the given filters. Without `--limit`, this returns *all* matching workflows, so that `dbosctl workflow list -o ids | dbosctl workflow cancel -` acts on the whole set. Pass `--limit`/`--offset` to bound or page the results.
**Arguments:**
- `-s, --status `: Filter by workflow status. Repeatable.
- `-n, --name `: Filter by workflow name. Repeatable.
- `--id `: Filter by workflow ID. Repeatable.
- `-u, --user `: Filter by user. Repeatable.
- `--queue `: Filter by queue name. Repeatable.
- `--queued`: Return only workflows currently on a queue.
- `--app-version `: Filter by application version. Repeatable.
- `--since `: Window start, as RFC 3339 or a duration such as `1h`.
- `--until `: Window end, as RFC 3339 or a duration such as `1h`.
- `--desc`: Sort newest first.
- `-l, --limit `: Maximum number of results.
- `--offset `: Number of results to skip.
---
#### `dbosctl workflow get`
**Description:**
Shows a workflow's details, including its input and output.
**Arguments:**
- ``: The workflow's ID.
---
#### `dbosctl workflow steps`
**Description:**
Lists a workflow's steps.
**Arguments:**
- ``: The workflow's ID.
---
#### `dbosctl workflow events`
**Description:**
Lists the events a workflow has set.
**Arguments:**
- ``: The workflow's ID.
---
#### `dbosctl workflow cancel`
**Description:**
Cancels one or more workflows. Cancelling sets a workflow's status to `CANCELLED`: if it is executing, its execution is preempted at the start of its next step; if it is enqueued, it is removed from the queue.
**Arguments:**
- `...`: One or more workflow IDs. A literal `-` reads IDs from stdin, one per line.
- `--children`: Also cancel the workflows' child workflows.
---
#### `dbosctl workflow resume`
**Description:**
Resumes one or more workflows from their last completed step. If a workflow is enqueued, resuming it bypasses the queue and starts it immediately.
**Arguments:**
- `...`: One or more workflow IDs. A literal `-` reads IDs from stdin.
- `--queue `: Resume onto this queue.
---
#### `dbosctl workflow fork`
**Description:**
Forks a workflow into a new execution starting from a chosen step, copying the original workflow's inputs and its completed steps up to that point. Prints the new workflow's ID.
**Arguments:**
- ``: The workflow to fork.
- `--start-step `: The step to fork from.
- `--new-id `: ID for the forked workflow. Generated if not supplied.
- `--app-version `: Application version to run the fork on.
- `--queue `: Enqueue the fork onto this queue.
---
#### `dbosctl workflow delete`
**Description:**
Deletes one or more workflows and their recorded history.
**Arguments:**
- `...`: One or more workflow IDs. A literal `-` reads IDs from stdin.
- `--children`: Also delete the workflows' child workflows.
---
### Queue Commands
#### `dbosctl queue list`
**Description:**
Lists an application's queue definitions.
---
#### `dbosctl queue get`
**Description:**
Shows one queue's details.
**Arguments:**
- ``: The queue's name.
---
### Schedule Commands
#### `dbosctl schedule list`
**Description:**
Lists an application's scheduled workflows.
---
#### `dbosctl schedule get`
**Description:**
Shows one schedule's details.
**Arguments:**
- ``: The schedule's name.
---
#### `dbosctl schedule pause`
**Description:**
Pauses a schedule, so that it stops starting new workflows.
**Arguments:**
- ``: The schedule's name.
---
#### `dbosctl schedule resume`
**Description:**
Resumes a paused schedule.
**Arguments:**
- ``: The schedule's name.
---
#### `dbosctl schedule trigger`
**Description:**
Runs a scheduled workflow immediately, out of band. Prints the started workflow's ID.
**Arguments:**
- ``: The schedule's name.
---
#### `dbosctl schedule backfill`
**Description:**
Replays a schedule across a past time window, starting one workflow for each occurrence the schedule would have fired. Prints the started workflow IDs.
**Arguments:**
- ``: The schedule's name.
- `--since `: Window start, as RFC 3339 or a duration such as `1h`.
- `--until `: Window end, as RFC 3339 or a duration such as `1h`.
---
### API Key Commands
The `token` and `apikey` aliases are accepted in place of `api-key`.
#### `dbosctl api-key list`
**Description:**
Lists the organization's API keys. Secrets are not shown.
---
#### `dbosctl api-key create`
**Description:**
Creates an API key and prints its secret. **The secret is shown once and cannot be retrieved afterwards.** By default the key is unscoped; narrow it with `--app` and `--permission`. See [Permissions and API Keys](../permissions.md).
**Arguments:**
- ``: A name for the key.
- `--app `: Scope the key to these applications. Repeatable; defaults to all applications.
- `--permission `: Grant these permissions, for example `application.read`. Repeatable.
---
#### `dbosctl api-key rename`
**Description:**
Renames an API key. The secret is unchanged, so anything already using the key keeps working.
**Arguments:**
- ``: The key's current name.
- ``: The key's new name. Fails if another key in the org already has it.
---
#### `dbosctl api-key delete`
**Description:**
Deletes an API key, revoking it immediately.
**Arguments:**
- ``: The key's name.
---
#### `dbosctl permission list`
**Description:**
Lists the permissions that can be granted to an API key or a role.
---
### System Database Commands
`dbosctl sysdb` groups the commands that open a database instead of calling Conductor.
They connect to a Postgres (or CockroachDB) [system database](../../explanations/system-tables.md) directly, so they take a database URL rather than a profile, and accept none of the [common flags](#common-flags) that resolve one.
The system schema is shared by every DBOS SDK and the migrations are built into the `dbosctl` binary.
**Shared arguments:**
- `-D, --db-url `: The system database URL. Overrides `$DBOS_SYSTEM_DATABASE_URL`.
- `--schema `: The schema holding the DBOS system tables. Defaults to `dbos`.
Both are defined on `sysdb` itself, so every subcommand takes them:
```shell
dbosctl sysdb migrate -D postgres://user:password@host:5432/dbos_sys
DBOS_SYSTEM_DATABASE_URL=postgres://user:password@host:5432/dbos_sys dbosctl sysdb migrate
```
---
#### `dbosctl sysdb migrate`
**Description:**
Creates or upgrades the DBOS [system database](../../explanations/system-tables.md), applying every migration the schema is missing and creating the database and schema if they do not exist yet.
By default, a DBOS application automatically creates these on startup.
However, in production environments, a DBOS application may not run with sufficient privilege to create databases or tables.
In that case, the `migrate` command can be run with a privileged user to create all DBOS database tables.
After creating the DBOS database tables with this command, a DBOS application can run with minimum permissions, requiring only access to the DBOS schema in the application and system databases.
Use the `-r/--app-role` flag to grant a role access to that schema.
`migrate` is safe to re-run. Migrations already recorded are skipped, so a database that is up to date is left alone.
**Arguments:**
- `-r, --app-role `: The role your DBOS application runs as. It is granted the minimum permissions needed to use the DBOS schema.
- `--no-listen-notify`: Leave out the triggers that fire `pg_notify`. See [LISTEN/NOTIFY](#listennotify) below.
- `--print-migrations `: Instead of running the migrations, print their SQL to standard output — `all` for a fresh database, or a migration number to upgrade an existing one.
- `--print-user-role`: Instead of running them, print the SQL statements granting `--app-role` access to the DBOS system tables.
- `--cockroach`: Render the printed SQL for CockroachDB. Print mode only — see [CockroachDB](#cockroachdb) below.
:::info SDK migration config settings
After running `dbosctl sysdb migrate`, configure your application not to alter the system schema on startup where that option exists:
**Python**
```python
config: DBOSConfig = {
"name": "my-app",
"system_database_url": os.environ["DBOS_SYSTEM_DATABASE_URL"],
"run_migrations": False,
}
```
**TypeScript**
```typescript
DBOS.setConfig({
name: "my-app",
systemDatabaseUrl: process.env.DBOS_SYSTEM_DATABASE_URL,
runMigrations: false,
});
await DBOS.launch();
```
**Java**
```java
DBOSConfig config = DBOSConfig.defaults("my-app")
.withDatabaseUrl(System.getenv("DBOS_SYSTEM_JDBC_URL"))
.withMigrate(false);
```
:::
##### Printing the SQL
If your database is managed by a DBA, or its DDL goes through review, the print modes emit SQL for someone else to apply:
```shell
dbosctl sysdb migrate --print-migrations all > migrations.sql
dbosctl sysdb migrate --print-user-role -r my_app_role > grants.sql
```
##### LISTEN/NOTIFY
If your system database sits behind a connection pooler in transaction mode, pass `--no-listen-notify` to generate a system schema that doesn't use `pg_notify`:
```shell
dbosctl sysdb migrate -D postgres://user:password@host:5432/dbos_sys --no-listen-notify
```
##### CockroachDB
Live migration can detect when the system database is running on CockroachDB.
Since a printed script cannot detect the database engine, use `--cockroach` to specify Cockroach compatible system database migrations.
```shell
dbosctl sysdb migrate --print-migrations all --cockroach > migrations.sql
```
---
#### `dbosctl sysdb reset`
**Description:**
Empties the DBOS system database, deleting all the rows in the DBOS system tables.
The schema itself is left migrated and immediately usable, so the database does not have to be provisioned again.
Prompts for confirmation when run interactively.
**Arguments:**
- `-a, --app `: Empty only the specified application's rows, for a [shared system database](../../explanations/sharing-a-system-database.md). Unlike the `--app` argument used by Conductor commands, this argument is only read from the command line — never from `$DBOS_APP` or a profile.
- `--drop-database`: Drop the whole database instead of emptying the DBOS tables. Cannot be combined with `--app` or `--schema`.
- `--force`: Skip the confirmation prompt. Required when running non-interactively.
- `-o, --output `: Output format for the row counts — `table` (default) or `json`.
When not using `--drop-database`, per-table row counts are written to standard output, with progress on standard error, so a scripted reset can read what it removed without parsing log lines.
---
#### `dbosctl sysdb rename-application`
**Description:**
Transfers ownership of everything in the system database from an application's old name to its new one.
The `rename-app` alias is accepted in place of `rename-application`.
Prints the number of rows transferred, by table.
Prompts for confirmation when run interactively.
:::warning
**Stop the application being renamed before running this.** Nothing here locks it out, and a running one keeps creating new records using its old name.
:::
**Arguments:**
- `-f, --from `: The application's previous name. Omit to only adopt unclaimed rows, which then requires `--adopt-unclaimed-rows`.
- `-t, --to `: The application that ends up owning the rows. Required. Must not be only whitespace.
- `--adopt-unclaimed-rows`: Also transfer rows no application owns (`application_name` is null).
- `--batch-size `: Completed workflows and steps transferred per transaction. Defaults to 10000.
- `--force`: Skip the confirmation prompt and the `--to` name checks. Required when running non-interactively.
- `-o, --output `: Output format for the row counts — `table` (default) or `json`.
By default, `rename-application` expects the `--to` name to be unused and to be DBOS Conductor compatible (between 3 and 256 characters, only numbers, lowercase ASCII letters, hyphens, and underscores).
For interactive shells, `rename-application` will confirm with the user before renaming if either of these conditions are false.
This check can be overridden with the `--force` argument.
##### Schema versions
`reset` and `rename-application` both refuse a schema migrated past what your `dbosctl` version knows.
Typically, in this case you just need to install a more recent version of `dbosctl`.
`rename-application` also refuses a schema that predates the addition of application names.
---
### Other Commands
#### `dbosctl version`
**Description:**
Prints the version, build metadata, and platform. Also available as `dbosctl --version`.
---
#### `dbosctl completion`
**Description:**
Generates a shell autocompletion script. Run `dbosctl completion --help` for installation instructions for your shell.
**Arguments:**
- ``: The shell to generate a script for: `bash`, `zsh`, `fish`, or `powershell`.
---
## Workflow Retention Policies(Conductor)
If you are using [Conductor](./overview.md), you can configure workflow history retention policies for your application from the Retention Policy page of the DBOS Console.
These settings let you configure how long workflow history is retained in your application's [system database](../explanations/system-tables.md).
This is useful for managing the database disk usage of workflow history.
Retention policies only delete the history of completed workflows (workflows with status `SUCCESS`, `ERROR`, `CANCELLED`, or `MAX_RECOVERY_ATTEMPTS_EXCEEDED`); workflows that are still running, enqueued, or delayed are never deleted.
Deleting a workflow's history also deletes its steps, inputs, outputs, messages, events, and streams.
Retention runs in the background and deletes history in batches.
If multiple applications [share a system database](../explanations/sharing-a-system-database.md), retention policies apply to the entire system database, including workflows owned by other applications.
The most restrictive policy configured by any of these applications therefore applies to all of them, so we recommend configuring the same retention policies for every application that shares a system database.
The global timeout applies only to workflows owned by the application for which it is configured.
#### Time Threshold
If a time threshold is set, workflow history is only retained for X hours after a workflow completes.
History of workflows that completed more than X hours ago is automatically deleted.
Time-based retention is disabled by default.
#### Rows Threshold
If the rows threshold is set, history is only retained for the X most recently completed workflows.
History of older completed workflows is automatically deleted.
Rows-based retention is disabled by default.
You can set both a rows threshold and a time threshold.
#### Global Timeout
If a global timeout is set, any workflow that has not completed X hours after it was created (started or enqueued) is automatically cancelled.
By default, the global timeout is disabled.
---
## Deploying Conductor on Kubernetes
:::info
Self-hosted Conductor is released under a [proprietary license](https://www.dbos.dev/conductor-license).
Self-hosting Conductor for commercial or production use requires a [license key](./hosting-conductor.md#licensing).
:::
### Overview
This guide covers deploying DBOS Conductor and the DBOS Console on Kubernetes.
It maps the components and production requirements from the [Self-Hosting Guide](./hosting-conductor.md) onto Kubernetes resources, then walks through a full deployment on AWS EKS.
The Kubernetes manifests are portable to any conformant cluster.
---
### Deployments
**Database** — Conductor's [Postgres database](./hosting-conductor.md#components) runs outside the cluster (this guide uses RDS).
**Conductor** — A single-container [Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/), not a [StatefulSet](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/), because all state lives in Postgres.
It listens on port 8090 and reads its [required environment variables](./hosting-conductor.md#conductor) from Secrets.
To run multiple replicas for [high availability](./hosting-conductor.md#high-availability), each pod must advertise its own address; `conductor.yaml` below sets `DBOS__ADVERTISE_ADDRESS` from the pod IP.
**Console** — A single-container Deployment listening on port 8080, behind a Service that publishes port 80.
Its `DBOS_CONDUCTOR_URL` is the in-cluster Conductor Service address, `conductor.dbos.svc.cluster.local:8090`.
:::info Updating Conductor
Conductor is architecturally **out-of-band** — it is not on the critical path of your application.
To upgrade, update the container image tag in `conductor.yaml` and `console.yaml`, (`latest` by default) then `kubectl rollout restart`. Prefer updating both Conductor and the console together.
Applications seamlessly reconnect to the new Conductor version with no impact on their availability.
:::
:::info Register applications
After deploying Conductor and Console, [register your application, and generate an API key](../overview.md#connecting-to-conductor).
The application connects to Conductor via WebSocket using this API key and the Conductor URL.
With the [Ingress](#ingress) below, that URL is your Ingress hostname plus the `/conductor-api` prefix:
```bash
DBOS_CONDUCTOR_KEY=
DBOS_CONDUCTOR_URL=wss:///conductor-api
```
Because this is a `wss://` connection, your application verifies the Ingress TLS certificate.
:::
### Authentication
Conductor supports OAuth 2.0 with any OIDC-compliant provider. See [Security](./hosting-conductor.md#security) for the provider setup and environment variables.
:::warning
Conductor performs **no authentication** unless OAuth is enabled, so anyone who can reach the Ingress has full admin access.
Configure OAuth before exposing this deployment to any untrusted network.
:::
When configuring your OAuth provider, the callback URL and allowed web origin are your Ingress hostname (`https:///oauth/callback` and `https://`).
The OAuth settings are not secrets, so they can be set directly in the Deployment manifests — Conductor and the Console each need their own set.
### Ingress
All external traffic enters through an Ingress that meets the [reverse proxy requirements](./hosting-conductor.md#reverse-proxy-and-tls) and routes by path: `/conductor-api/...` to Conductor, everything else to the Console.
This guide uses [ingress-nginx](https://kubernetes.github.io/ingress-nginx/), but any ingress controller meeting those requirements will work. The `ingress.yaml` below defines the routing it must implement.
Set idle timeouts to 3600 seconds on both the ingress controller and the cloud load balancer in front of it (for example, AWS ELB).
### Security Best Practices
**Secret management** — Store the [Conductor secrets](./hosting-conductor.md#secrets) as Kubernetes Secrets and inject them via `secretKeyRef`.
For Git-safe storage, encrypt with [Sealed Secrets](https://github.com/bitnami-labs/sealed-secrets), [SOPS](https://github.com/getsops/sops), or a cloud-native secrets manager (AWS Secrets Manager, [Vault](https://developer.hashicorp.com/vault/docs/platform/k8s/vso), etc.).
**Network policies** — Apply a default-deny ingress policy to the namespace, then add explicit allow rules for each pod. If Conductor and Console are co-located, allow traffic from the Console to Conductor on port 8090.
Keep [outbound HTTPS](./hosting-conductor.md#network-access) open from the Conductor pod for license validation, which means its nodes need a route to the internet, such as a NAT gateway for private subnets.
**RBAC** — Restrict which ServiceAccounts can read Secrets in the namespace. Conductor credentials (database URLs, license key, API key) should only be accessible to the pods that need them.
---
### Walkthrough (AWS EKS)
**EKS (AWS)**
In addition to DBOS Conductor and the DBOS Console, the infrastructure includes the following components:
| Component | Role |
|-----------|------|
| **RDS** | Database for Conductor operating state |
| **Reverse Proxy (Nginx Ingress)** | TLS termination, path-based routing, WebSocket support |
| **Sealed Secrets** | Encrypts secrets at rest; decrypts them in-cluster |
Set environment variables
Set these variables before proceeding — replace the placeholder values with your own:
```bash
# Your AWS account ID (12-digit number)
AWS_ACCOUNT_ID=123456789012
# AWS region for all resources
AWS_REGION=us-west-2
# PostgreSQL admin password (used for the RDS master user)
POSTGRES_PASSWORD='choose-a-secure-password'
# Password for the Conductor database role
CONDUCTOR_ROLE_PASSWORD='choose-another-secure-password'
# Conductor license key (from DBOS Console or sales)
CONDUCTOR_LICENSE_KEY='your-license-key'
```
#### Infrastructure
CLI tools required on your workstation
| Tool | Purpose | Install |
|------|---------|---------|
| **AWS CLI** | AWS account access | [Install guide](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) |
| **eksctl** | Create and manage EKS clusters | [Install guide](https://eksctl.io/installation/) |
| **kubectl** | Interact with Kubernetes | Included with eksctl, or [install separately](https://kubernetes.io/docs/tasks/tools/) |
| **Helm** | Install cluster add-ons (Ingress, Sealed Secrets) | `brew install helm` or [Install guide](https://helm.sh/docs/intro/install/) |
| **kubeseal** | Encrypt Kubernetes secrets | `brew install kubeseal` or [Install guide](https://github.com/bitnami-labs/sealed-secrets#kubeseal) |
| **openssl** | Generate self-signed TLS certificate | Pre-installed on macOS/Linux |
Verify your AWS credentials are configured:
```bash
aws sts get-caller-identity
```
**DBOS Conductor License Key**
Obtain a development license key from the [DBOS Console](https://console.dbos.dev/settings/license-key) or [contact DBOS sales](https://www.dbos.dev/contact) for a pro license key.
You can follow this guide with a development license key for evaluation, but you will be limited to one executor per application.
**Create an EKS Cluster**
Create a managed EKS cluster with two nodes. This takes approximately 15 minutes.
Create EKS cluster
```bash
eksctl create cluster \
--name dbos-conductor \
--region $AWS_REGION \
--version 1.32 \
--nodegroup-name default \
--node-type t3.medium \
--nodes 2 \
--managed
```
`eksctl` automatically:
- Creates a VPC with public and private subnets
- Configures the [Amazon VPC CNI](https://docs.aws.amazon.com/eks/latest/userguide/managing-vpc-cni.html), which supports NetworkPolicy enforcement
- Sets up your `~/.kube/config` to point at the new cluster
Once complete, verify the cluster is ready:
```bash
kubectl get nodes
```
You should see two nodes in `Ready` status:
```
NAME STATUS ROLES AGE VERSION
ip-192-168-xx-xx.us-west-2.compute.internal Ready 2m v1.32.x
ip-192-168-xx-xx.us-west-2.compute.internal Ready 2m v1.32.x
```
**Create a Namespace**
All resources in this guide are deployed to a dedicated `dbos` namespace:
```bash
kubectl create namespace dbos
```
**Provision an RDS PostgreSQL Instance**
RDS provisioning commands
Find the VPC and private subnets that `eksctl` created:
```bash
# Get the VPC ID
VPC_ID=$(aws ec2 describe-vpcs \
--filters "Name=tag:alpha.eksctl.io/cluster-name,Values=dbos-conductor" \
--query "Vpcs[0].VpcId" --output text --region $AWS_REGION)
echo "VPC: $VPC_ID"
# Get the private subnets
PRIVATE_SUBNETS=($(aws ec2 describe-subnets \
--filters "Name=vpc-id,Values=$VPC_ID" \
"Name=tag:aws:cloudformation:logical-id,Values=SubnetPrivate*" \
--query "Subnets[*].SubnetId" --output text --region $AWS_REGION))
echo "Private subnets: ${PRIVATE_SUBNETS[@]}"
```
Create a DB subnet group from the private subnets:
```bash
aws rds create-db-subnet-group \
--db-subnet-group-name dbos-conductor-db \
--db-subnet-group-description "DBOS Conductor RDS subnets" \
--subnet-ids "${PRIVATE_SUBNETS[@]}" \
--region $AWS_REGION
```
Create a security group that allows PostgreSQL access from the EKS nodes:
```bash
# Get the EKS cluster security group
EKS_SG=$(aws ec2 describe-security-groups \
--filters "Name=vpc-id,Values=$VPC_ID" \
"Name=tag:aws:eks:cluster-name,Values=dbos-conductor" \
--query "SecurityGroups[0].GroupId" \
--output text --region $AWS_REGION)
echo "EKS SG: $EKS_SG"
# Create a security group for RDS
RDS_SG=$(aws ec2 create-security-group \
--group-name dbos-conductor-rds \
--description "Allow PostgreSQL from EKS nodes" \
--vpc-id $VPC_ID \
--query "GroupId" --output text --region $AWS_REGION)
echo "RDS SG: $RDS_SG"
# Allow inbound PostgreSQL from EKS nodes
aws ec2 authorize-security-group-ingress \
--group-id $RDS_SG \
--protocol tcp --port 5432 \
--source-group $EKS_SG \
--region $AWS_REGION
```
Create the RDS instance:
```bash
aws rds create-db-instance \
--db-instance-identifier dbos-conductor-pg \
--db-instance-class db.t4g.micro \
--engine postgres \
--engine-version 16 \
--master-username postgres \
--master-user-password "$POSTGRES_PASSWORD" \
--allocated-storage 20 \
--db-subnet-group-name dbos-conductor-db \
--vpc-security-group-ids $RDS_SG \
--no-publicly-accessible \
--region $AWS_REGION
```
Wait for the instance to become available (this takes a few minutes):
```bash
aws rds wait db-instance-available \
--db-instance-identifier dbos-conductor-pg \
--region $AWS_REGION
```
Get the RDS endpoint:
```bash
RDS_ENDPOINT=$(aws rds describe-db-instances \
--db-instance-identifier dbos-conductor-pg \
--query "DBInstances[0].Endpoint.Address" \
--output text --region $AWS_REGION)
echo "RDS endpoint: $RDS_ENDPOINT"
```
Create the databases and roles from a pod inside the cluster (since the RDS instance is not publicly accessible):
Create databases and roles
```bash
kubectl run pg-setup --restart=Never \
--namespace dbos \
--image=postgres:16 \
--env="PGPASSWORD=$POSTGRES_PASSWORD" \
--command -- bash -c "
psql -h $RDS_ENDPOINT -U postgres -c 'CREATE DATABASE dbos_conductor;'
psql -h $RDS_ENDPOINT -U postgres -c \"CREATE ROLE dbos_conductor_role WITH LOGIN PASSWORD '$CONDUCTOR_ROLE_PASSWORD';\"
psql -h $RDS_ENDPOINT -U postgres -c 'GRANT ALL PRIVILEGES ON DATABASE dbos_conductor TO dbos_conductor_role;'
psql -h $RDS_ENDPOINT -U postgres -d dbos_conductor -c 'GRANT ALL ON SCHEMA public TO dbos_conductor_role;'
"
# Wait for the pod to finish, then clean up
sleep 15 && kubectl logs pg-setup -n dbos && kubectl delete pod pg-setup -n dbos
```
This creates:
- `dbos_conductor` — Conductor's internal database (application registry, metadata)
- `dbos_conductor_role` — a dedicated role for Conductor's database access
**Install Cluster Add-ons**
We install two Helm charts that the later sections depend on.
Helm installs (Nginx Ingress, Sealed Secrets)
**Nginx Ingress Controller** — reverse proxy and TLS termination:
```bash
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
helm install ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx --create-namespace \
--set controller.service.type=LoadBalancer
```
**Sealed Secrets** — encrypt secrets for safe Git storage:
```bash
helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets
helm install sealed-secrets sealed-secrets/sealed-secrets \
--namespace kube-system
```
Verify all add-ons are running:
```bash
# Ingress controller
kubectl get pods -n ingress-nginx
# Sealed Secrets controller
kubectl get pods -n kube-system -l app.kubernetes.io/name=sealed-secrets
```
#### Secrets
Several components need sensitive credentials.
We use [Bitnami Sealed Secrets](https://github.com/bitnami-labs/sealed-secrets): create a regular Secret, encrypt it with `kubeseal`, and apply the encrypted `SealedSecret` to the cluster.
The controller decrypts it in-cluster into a standard Kubernetes Secret that pods can reference.
The encrypted form is safe to commit to Git.
**Secrets Inventory**
| Secret | Keys | Used by |
|--------|------|---------|
| `conductor-db` | `database-url` | Conductor — connection to `dbos_conductor` database |
| `conductor-license` | `license-key` | Conductor — production license |
**Create and Seal Secrets**
kubeseal commands
Create each secret, pipe it through `kubeseal`, and save the encrypted form:
```bash
# 1. Conductor database credentials (dedicated role)
kubectl create secret generic conductor-db \
--namespace dbos \
--from-literal=database-url="postgresql://dbos_conductor_role:${CONDUCTOR_ROLE_PASSWORD}@${RDS_ENDPOINT}:5432/dbos_conductor?sslmode=require" \
--dry-run=client -o yaml | \
kubeseal --controller-name=sealed-secrets --controller-namespace=kube-system --format yaml \
> sealed-conductor-db.yaml
# 2. Conductor license key
kubectl create secret generic conductor-license \
--namespace dbos \
--from-literal=license-key="$CONDUCTOR_LICENSE_KEY" \
--dry-run=client -o yaml | \
kubeseal --controller-name=sealed-secrets --controller-namespace=kube-system --format yaml \
> sealed-conductor-license.yaml
```
**Apply and Verify**
```bash
kubectl apply -f sealed-conductor-db.yaml
kubectl apply -f sealed-conductor-license.yaml
```
Verify the controller has decrypted them into regular Kubernetes Secrets:
```bash
kubectl get secrets -n dbos
```
```
NAME TYPE DATA AGE
conductor-db Opaque 1 10s
conductor-license Opaque 1 10s
```
#### Ingress
With the Nginx Ingress Controller installed, you have a load balancer in front of the cluster.
This section creates a TLS certificate and an Ingress resource so that all services are reachable over HTTPS.
This walkthrough uses a self-signed certificate on the load balancer's hostname.
For production, use [cert-manager](https://cert-manager.io/) with a real domain.
Get the load balancer hostname:
```bash
ELB_HOSTNAME=$(kubectl get svc -n ingress-nginx ingress-nginx-controller \
-o jsonpath='{.status.loadBalancer.ingress[0].hostname}')
echo $ELB_HOSTNAME
```
Save this value — you'll need it throughout the rest of the guide. It looks like `xxxxxxxx.us-west-2.elb.amazonaws.com`.
Create a self-signed TLS certificate
```bash
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout tls.key -out tls.crt \
-subj "/CN=dbos-conductor" \
-addext "subjectAltName=DNS:${ELB_HOSTNAME}"
kubectl create secret tls dbos-tls \
--cert=tls.crt --key=tls.key \
--namespace dbos
```
:::note
The CN is kept short because OpenSSL's CN field has a 64-character limit — the actual hostname is covered by the SAN extension.
Your browser will show a certificate warning for the self-signed cert — accept it to proceed.
:::
:::warning Applications must trust this certificate
Applications connect to Conductor over `wss://`, so they verify the Ingress certificate and will fail the TLS handshake against one they do not trust.
**For production**, issue a certificate for a domain you control, for example with [cert-manager](https://cert-manager.io/).
Note that no public CA will issue a certificate for an `*.elb.amazonaws.com` hostname, so this requires your own domain pointed at the load balancer.
**To evaluate with the self-signed certificate**, your applications must be configured to trust it.
Distribute it using a ConfigMap and configure `SSL_CERT_FILE` (Go, Python) / `NODE_EXTRA_CA_CERTS` (TypeScript) or the JDK `javax.net.ssl.trustStore` (Java) accordingly.
:::
ingress.yaml
The Ingress routes `/conductor-api/...` to the Conductor service and everything else to the Console.
A regex rewrite strips the `/conductor-api` prefix so Conductor sees requests at `/`.
Replace `` with the `$ELB_HOSTNAME` value you retrieved above.
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: dbos-ingress
namespace: dbos
annotations:
nginx.ingress.kubernetes.io/use-regex: "true"
nginx.ingress.kubernetes.io/rewrite-target: /$2
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
spec:
ingressClassName: nginx
tls:
- hosts:
-
secretName: dbos-tls
rules:
- host:
http:
paths:
# Both paths are regexes, so ordering matters: ingress-nginx sorts
# locations longest-path-first, which puts /conductor-api ahead of
# the Console catch-all.
- path: /conductor-api(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: conductor
port:
number: 8090
- path: /()(.*)
pathType: ImplementationSpecific
backend:
service:
name: console
port:
number: 80
```
The `host` in both `tls` and `rules` must match — without it, Nginx serves its default fake certificate instead of `dbos-tls`.
| Request path | Backend |
|---|---|
| `/conductor-api/websocket//` | conductor:8090 → `/websocket//` |
| `/conductor-api/healthz` | conductor:8090 → `/healthz` |
| `/conductor-api/v1/metrics` | conductor:8090 → `/v1/metrics` |
| `/` | console:80 |
| `/conductor/applications` | console:80 (UI page) |
- **`rewrite-target: /$2`** — strips the `/conductor-api` prefix using the second capture group. The Console catch-all uses `/()(.*)` so `$2` passes the full path through unchanged.
- **`proxy-read-timeout` / `proxy-send-timeout`** — set to 3600s to keep Conductor's long-lived WebSocket connections alive.
**Apply the Ingress**
```bash
kubectl apply -f ingress.yaml
```
**WebSocket Configuration**
The application connects to Conductor via a long-lived WebSocket.
Three layers must be configured to prevent idle connections from being dropped:
| Layer | Setting | Default | Suggested | Why |
|-------|---------|---------|----------|-----|
| **Nginx Ingress** | `proxy-read-timeout` | 60s | 3600s | Prevents Nginx from closing an idle WebSocket |
| **Nginx Ingress** | `proxy-send-timeout` | 60s | 3600s | Same, for the send direction |
| **AWS ELB** | idle timeout | 60s | 3600s | Prevents the load balancer from closing an idle TCP connection |
The Nginx timeouts are already set via the Ingress annotations.
Nginx handles the `Connection: Upgrade` and `Upgrade: websocket` headers automatically — no additional annotation is needed for the protocol upgrade itself.
The AWS load balancer idle timeout is configured separately on the `ingress-nginx-controller` Service:
```bash
kubectl patch svc ingress-nginx-controller -n ingress-nginx -p \
'{"metadata":{"annotations":{"service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout":"3600"}}}'
```
:::note
The DBOS SDK sends periodic ping frames that keep the connection active under normal conditions.
Albeit the SDK will reconnect automatically, increasing the ELB idle timeout will prevent network hiccups from dropping the connection.
:::
#### Deployments
Conductor is the core service that manages workflow recovery and the application registry.
It connects to the `dbos_conductor` database using the `dbos_conductor_role` credentials.
conductor.yaml
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: conductor
namespace: dbos
spec:
replicas: 1
selector:
matchLabels:
app: conductor
template:
metadata:
labels:
app: conductor
spec:
containers:
- name: conductor
# Untagged resolves to :latest. For production, pin an explicit
# version so rollouts are reproducible: dbosdev/conductor:
image: dbosdev/conductor
env:
- name: DBOS__CONDUCTOR_DB_URL
valueFrom:
secretKeyRef:
name: conductor-db
key: database-url
- name: DBOS_CONDUCTOR_LICENSE_KEY
valueFrom:
secretKeyRef:
name: conductor-license
key: license-key
# Peers forward tasks to each other at this address. It defaults to
# 127.0.0.1, which only works for a single replica — set it to the
# pod IP before scaling up.
- name: DBOS__ADVERTISE_ADDRESS
valueFrom:
fieldRef:
fieldPath: status.podIP
ports:
- containerPort: 8090
readinessProbe:
httpGet:
path: /healthz
port: 8090
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /healthz
port: 8090
initialDelaySeconds: 15
periodSeconds: 30
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: "1"
memory: 512Mi
---
apiVersion: v1
kind: Service
metadata:
name: conductor
namespace: dbos
spec:
selector:
app: conductor
ports:
- port: 8090
targetPort: 8090
```
Both sensitive values (`DBOS__CONDUCTOR_DB_URL` and `DBOS_CONDUCTOR_LICENSE_KEY`) are pulled from the Sealed Secrets created in the [Secrets](#secrets) section.
The Console is the web UI for managing applications, monitoring workflows, and generating API keys.
In this example, it connects to Conductor via internal cluster DNS.
console.yaml
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: console
namespace: dbos
spec:
replicas: 1
selector:
matchLabels:
app: console
template:
metadata:
labels:
app: console
spec:
containers:
- name: console
# As with Conductor, pin an explicit version in production and keep
# the two in step: dbosdev/console:
image: dbosdev/console
env:
- name: DBOS_CONDUCTOR_URL
value: "conductor.dbos.svc.cluster.local:8090"
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 10
periodSeconds: 30
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
---
apiVersion: v1
kind: Service
metadata:
name: console
namespace: dbos
spec:
selector:
app: console
ports:
- port: 80
targetPort: 8080
```
Deploy both with:
```bash
kubectl apply -f conductor.yaml
kubectl apply -f console.yaml
```
Verify both pods are running:
```bash
kubectl get pods -n dbos
```
```
NAME READY STATUS RESTARTS AGE
conductor-xxxxxxxxx-xxxxx 1/1 Running 0 2m
console-xxxxxxxxx-xxxxx 1/1 Running 0 30s
```
**Access the Console and Generate an API Key**
At this point, your self-hosted Conductor deployment is fully operational! Open `https:///` in your browser (accept the self-signed cert warning), then follow the [Conductor setup instructions](../overview.md#connecting-to-conductor) to:
1. Register your application
2. Generate an API key
#### Cleanup
To tear down all AWS resources when done, delete them in this order.
```bash
# 1. Delete the RDS instance and wait for it to be gone
aws rds delete-db-instance --db-instance-identifier dbos-conductor-pg \
--skip-final-snapshot --region $AWS_REGION
aws rds wait db-instance-deleted \
--db-instance-identifier dbos-conductor-pg \
--region $AWS_REGION
# 2. Delete the DB subnet group (must be empty)
aws rds delete-db-subnet-group --db-subnet-group-name dbos-conductor-db --region $AWS_REGION
# 3. Delete the RDS security group (no longer attached to any instance)
RDS_SG=$(aws ec2 describe-security-groups \
--filters "Name=group-name,Values=dbos-conductor-rds" \
--query "SecurityGroups[0].GroupId" --output text --region $AWS_REGION)
aws ec2 delete-security-group --group-id $RDS_SG --region $AWS_REGION
# 4. Delete the EKS cluster (includes VPC, security groups, and node group)
eksctl delete cluster --name dbos-conductor --region $AWS_REGION
```
---
## Self-Hosting Guide
:::info
Self-hosted Conductor is released under a [proprietary license](https://www.dbos.dev/conductor-license) and requires a [license key](#licensing).
:::
You can self-host Conductor and the DBOS Console on any infrastructure that runs containers.
This guide covers what a self-hosted deployment consists of and what it needs in production, independent of where you run it.
See our [Kubernetes guide](./hosting-conductor-with-kubernetes.md) for a specific walkthrough.
### Components
A self-hosted deployment has three parts:
| Component | Image | Port | Role |
|---|---|---|---|
| **Conductor** | [`dbosdev/conductor`](https://hub.docker.com/r/dbosdev/conductor) | 8090 | The control plane your applications connect to over WebSocket. |
| **DBOS Console** | [`dbosdev/console`](https://hub.docker.com/r/dbosdev/console) | 8080 | Conductor's web UI. |
| **Postgres** | Any Postgres | 5432 | Conductor's own database, holding its registry of applications, users, and settings. |
Conductor's database is separate from the system databases your DBOS applications use.
Conductor never connects to your applications' databases; it exchanges workflow metadata and commands with your applications over their WebSocket connections.
In addition to the Console, you can use [Conductor's API](../reference/conductor-api.md) and the [dbosctl CLI](../reference/dbosctl.md) to manage your applications and their workflows.
### Trying It Locally with Docker Compose
For development and trial purposes, you can self-host Conductor and the DBOS Console on your development machine using Docker Compose.
To do this, you need a development license key, which can be obtained from the DBOS Console [here](https://console.dbos.dev/settings/license-key).
See [licensing](#licensing) for more information.
You should export this license key as an environment variable:
```shell
export DBOS_CONDUCTOR_LICENSE_KEY=
```
You can trial self-hosted Conductor with this `docker-compose.yml`:
docker-compose.yml
```yml title="docker-compose.yml"
# Docker Compose configuration for self-hosting DBOS Conductor and the DBOS Console.
# This configuration is for development purposes only.
# Commercial or production use of DBOS Conductor or the DBOS Console requires a paid license key.
services:
# ============================================
# Postgres
# ============================================
postgres:
image: postgres:16
container_name: dbos-postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${PGPASSWORD:-dbos}
POSTGRES_DB: dbos_conductor
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- dbos-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
# ============================================
# Conductor
# ============================================
conductor:
image: dbosdev/conductor
container_name: dbos-conductor
environment:
DBOS__CONDUCTOR_DB_URL: postgresql://postgres:${PGPASSWORD:-dbos}@postgres:5432/dbos_conductor?sslmode=disable
# License Key (required)
DBOS_CONDUCTOR_LICENSE_KEY: ${DBOS_CONDUCTOR_LICENSE_KEY}
# OAuth configuration
# DBOS_OAUTH_ENABLED: "true"
# DBOS_OAUTH_ISSUER: "https://your-oauth-provider.com/"
# DBOS_OAUTH_AUDIENCE: "your-api-audience"
ports:
- "8090:8090"
depends_on:
postgres:
condition: service_healthy
networks:
- dbos-network
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:8090/healthz']
interval: 30s
timeout: 3s
retries: 3
start_period: 5s
# ============================================
# DBOS Console
# ============================================
console:
image: dbosdev/console
container_name: dbos-console
environment:
# Conductor URL (defaults to conductor:8090 for same Docker network)
# Override to connect to remote Conductor
# DBOS_CONDUCTOR_URL=conductor.example.com:8090 (remote)
DBOS_CONDUCTOR_URL: '${DBOS_CONDUCTOR_URL:-conductor:8090}'
# OAuth configuration (uncomment and configure to enable authentication)
# DBOS_OAUTH_ENABLED: 'true'
# DBOS_OAUTH_AUTHORIZATION_URL: 'https://your-oauth-provider.com/[...]/authorize'
# DBOS_OAUTH_TOKEN_URL: 'https://your-oauth-provider.com/[...]/token'
# DBOS_OAUTH_CLIENT_ID: 'your-client-id'
# DBOS_OAUTH_SCOPE: 'openid profile email'
# DBOS_OAUTH_USERINFO_URL: 'https://your-oauth-provider.com/[...]/userinfo'
# DBOS_OAUTH_LOGOUT_URL: 'https://your-oauth-provider.com/[...]/logout'
# DBOS_OAUTH_AUDIENCE: 'your-api-identifier'
ports:
# Expose console on port 80 (or override with DBOS_CONSOLE_PORT env var)
- '${DBOS_CONSOLE_PORT:-80}:8080'
depends_on:
conductor:
condition: service_healthy
networks:
- dbos-network
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:8080/health']
interval: 30s
timeout: 3s
retries: 3
start_period: 5s
# ============================================
# Networks
# ============================================
networks:
dbos-network:
driver: bridge
name: dbos-network
# ============================================
# Volumes
# ============================================
volumes:
postgres_data:
```
Start Conductor and the DBOS Console with `docker compose up`.
After all containers have launched, navigate to http://localhost to view the self-hosted console.
### Connecting Applications
To connect your application to self-hosted Conductor, first [follow these steps](../overview.md#connecting-to-conductor) in your self-hosted DBOS Console to register an application, generate an API key, and set it in your application.
:::tip
When self-hosting Conductor, make sure you register your application and generate your key in your self-hosted console, not at https://console.dbos.dev.
:::
Then, provide your application with a websockets URL to your self-hosted Conductor server.
For example, for the Docker Compose setup above, this URL is `ws://localhost:8090/`.
In production, use a `wss://` URL that goes through your [reverse proxy](#reverse-proxy-and-tls).
**Python**
```python
config: DBOSConfig = {
"name": "my-app-name",
"application_version": "0.1.0",
"system_database_url": os.environ.get("DBOS_SYSTEM_DATABASE_URL"),
"conductor_key": os.environ.get("DBOS_CONDUCTOR_KEY"),
"conductor_url": os.environ.get("DBOS_CONDUCTOR_URL"),
}
DBOS(config=config)
```
**TypeScript**
```typescript
DBOS.setConfig({
"name": "my-app-name",
"applicationVersion": "0.1.0",
"systemDatabaseUrl": process.env.DBOS_SYSTEM_DATABASE_URL,
});
const conductorKey = process.env.DBOS_CONDUCTOR_KEY;
const conductorURL = process.env.DBOS_CONDUCTOR_URL;
await DBOS.launch({conductorKey, conductorURL});
```
**Go**
```go
conductorKey := os.Getenv("DBOS_CONDUCTOR_KEY")
conductorURL := os.Getenv("DBOS_CONDUCTOR_URL")
dbosContext, err := dbos.NewDBOSContext(context.Background(), dbos.Config{
AppName: "dbos-starter",
ApplicationVersion: "0.1.0",
DatabaseURL: os.Getenv("DBOS_SYSTEM_DATABASE_URL"),
ConductorURL: conductorURL,
ConductorAPIKey: conductorKey,
})
```
**Java**
```java
String conductorKey = System.getenv("DBOS_CONDUCTOR_KEY");
String conductorDomain = System.getenv("DBOS_CONDUCTOR_URL");
DBOSConfig config = DBOSConfig.defaults("dbos-java-starter")
.withAppVersion("0.1.0")
.withDatabaseUrl(System.getenv("DBOS_SYSTEM_JDBC_URL"))
.withConductorKey(conductorKey)
.withConductorDomain(conductorDomain);
```
### Licensing
For development, testing, or evaluation purposes, you can obtain a trial Conductor key from the [DBOS Console](https://console.dbos.dev/settings/license-key). A license agreement is required for production use. To obtain a production license, please [contact sales](https://www.dbos.dev/contact).
You can provide your key to Conductor using the `DBOS_CONDUCTOR_LICENSE_KEY` environment variable.
### Deploying to Production
The Docker Compose setup above is for development only.
A production deployment runs the same containers with a managed Postgres database, a reverse proxy, secret storage, and [authentication](#security).
:::tip
For a complete production deployment, including infrastructure, secrets, ingress, and authentication, follow the [Kubernetes guide](./hosting-conductor-with-kubernetes.md).
The requirements below apply to any platform.
:::
#### Conductor
Run Conductor as a stateless container service with an orchestrator like Kubernetes, ECS, Cloud Run, Nomad, or plain VMs.
Because all state lives in Postgres, instances are interchangeable, and you can run several for [high availability](#high-availability).
Conductor requires these environment variables:
| Environment variable | Description |
|---|---|
| `DBOS__CONDUCTOR_DB_URL` | Connection string for Conductor's Postgres database. We recommend a dedicated database role. |
| `DBOS_CONDUCTOR_LICENSE_KEY` | Your [license key](#licensing). |
#### DBOS Console
Run the Console as a stateless container service listening on port 8080.
Set `DBOS_CONDUCTOR_URL` in the Console container to the bare `host:port` of your Conductor service (for example, `conductor.internal:8090`).
This differs from the `DBOS_CONDUCTOR_URL` your applications use, which is a full WebSocket URL.
Without [OAuth authentication](#security), the Console has no user or organization management.
#### Reverse Proxy and TLS
Place Conductor and the Console behind a reverse proxy or load balancer (such as Nginx, an ingress controller, or a cloud load balancer) that **supports WebSockets** and does **TLS termination** (Conductor and the Console serve plain HTTP). Route traffic to Conductor on port 8090 and to the Console on port 8080.
We recommend setting long idle timeouts on the proxy and on any load balancer in front of it to handle network hiccups (for example, 3600 seconds). The DBOS SDK sends periodic pings and reconnects automatically after a disconnect.
#### Network Access
- **Outbound HTTPS from Conductor.** Conductor validates its license key against `https://cloud.dbos.dev` at startup and exits if it cannot reach it. Hosts in private networks need a route to the internet, such as a NAT gateway. For air-gapped deployments, [contact sales](https://www.dbos.dev/contact).
- **Console to Conductor.** The Console must reach Conductor on port 8090.
- **Conductor to Conductor.** In a [highly available](#high-availability) deployment, Conductor instances must reach each other directly.
Conductor never needs access to your applications' databases, and your applications need only outbound access to the reverse proxy.
#### Secrets
Conductor's database URL and license key are secrets.
Store them in your platform's secret store (such as Kubernetes Secrets, AWS Secrets Manager, or Vault) and inject them as environment variables.
The Conductor API keys your applications use to connect are also secrets, and belong in each application's secret store.
The OAuth settings below are not secrets and can be set directly in your deployment configuration.
### High Availability
For production deployments that require fault tolerance, you can run multiple Conductor instances in a highly available configuration.
All Conductor instances connect to the same Postgres database, which holds all Conductor state, so you can run multiple Conductor instances in multiple availability zones (or other failure domains) behind a load balancer.
In a highly available configuration, you should additionally use a highly available Postgres database, such as AWS RDS or Aurora in a multi-AZ replicated configuration, or equivalent offerings from other Postgres providers.
#### How It Works
Each of your DBOS application's executors maintains a long-lived WebSocket connection to Conductor.
When you run multiple Conductor instances behind a load balancer, the load balancer distributes these connections across instances, so each executor connects to (and is owned by) exactly one Conductor instance at a time.
When a request (for example, from the DBOS Console) needs to reach a particular executor, it may land on any Conductor instance.
If that instance does not own the target executor's connection, it looks up the owning instance in Postgres and forwards the request to it directly.
The owning instance then relays the request to the executor over its WebSocket.
This means **Conductor instances must be able to reach each other directly over the network**, in addition to being reachable through the load balancer.
This peer-to-peer traffic flows directly between instances.
To enable peer forwarding, each Conductor instance must advertise an address that its peers can use to reach it.
Set the `DBOS__ADVERTISE_ADDRESS` environment variable to a routable address (a hostname or IP, without a port); peers connect to this address on the Conductor port (`8090` by default, configurable with `DBOS__CONDUCTOR_PORT`).
#### Configuration summary
| Environment variable | Default | Description |
|---|---|---|
| `DBOS__ADVERTISE_ADDRESS` | `127.0.0.1` | Routable address or URL peers use to forward requests to this instance. **Must be set** for multi-instance deployments. |
| `DBOS__CONDUCTOR_PORT` | `8090` | Port Conductor listens on and advertises to peers. |
### Security
To securely self-host Conductor in production, you should set up authentication and authorization for all API calls made to it.
:::warning
Conductor performs **no authentication** unless OAuth is enabled.
Without it, all API requests run as a built-in `local` organization admin, and Conductor does not verify API keys on incoming WebSocket connections.
Anyone who can reach Conductor can register applications, cancel, resume, fork, or delete workflows, and create API keys.
Configure OAuth before exposing Conductor to any untrusted network.
:::
You can integrate Conductor with any OAuth-compatible single-sign on (SSO) experience.
To do this, first register the DBOS Console as an application and Conductor as an API (audience) with your OAuth provider.
Configure the following with your provider:
- `https://your-domain/oauth/callback` as a callback URL
- `https://your-domain` as an allowed web origin
- Authorization code with PKCE as an allowed grant type
- `openid profile email` as valid scopes.
Then, set these environment variables in your Conductor container:
```yml
DBOS_OAUTH_ENABLED: "true"
DBOS_OAUTH_ISSUER: "https://your-oauth-provider.com/"
DBOS_OAUTH_AUDIENCE: "your-api-audience"
```
And set these environment variables in your DBOS Console container:
```yml
DBOS_OAUTH_ENABLED: 'true'
DBOS_OAUTH_AUTHORIZATION_URL: 'https://your-oauth-provider.com/[...]/authorize'
DBOS_OAUTH_TOKEN_URL: 'https://your-oauth-provider.com/[...]/token'
DBOS_OAUTH_CLIENT_ID: 'your-client-id'
DBOS_OAUTH_SCOPE: 'openid profile email'
DBOS_OAUTH_USERINFO_URL: 'https://your-oauth-provider.com/[...]/userinfo'
DBOS_OAUTH_LOGOUT_URL: 'https://your-oauth-provider.com/[...]/logout'
DBOS_OAUTH_AUDIENCE: 'your-api-audience'
```
These values correspond to the client credentials and endpoints provided by your OAuth identity provider (such as Google, Auth0, or Okta).
None of these values are secrets.
When properly configured, the DBOS Console will redirect users to your SSO login page and enforce authentication on access.
This will also enable user and organization management features.
### Upgrading
You can upgrade Conductor and the DBOS Console by simply upgrading the container versions and restarting the service.
Because Conductor is entirely out-of-band, this will have no impact on your DBOS applications' availability; your apps will seamlessly reconnect to your new Conductor version.
We recommend regularly upgrading Conductor and the DBOS Console to the latest versions to take advantage of new features.
We always guarantee it is safe to upgrade directly from any past version to any future version.
For the best experience, we recommend upgrading Conductor and the DBOS Console together and not using a version of the DBOS Console more recent than your version of Conductor.
### Scaling
Architecturally, Conductor is entirely off your workflows orchestration path.
As such, it requires minimal resources to serve large application deployments.
A single server hosting the Conductor service can serve tens of thousands of application servers processing millions of workflows per second.
---
## Workflow Management(Conductor)
:::info
Workflow observability and management features are only available for applications connected to [Conductor](./overview.md).
:::
### Viewing Workflows
Navigate to the workflows tab of your application's page on the DBOS Console to see a list of its workflows:
This includes **all** your application's workflows: those currently executing, those enqueued for execution, those that have completed successfully, and those that have failed.
You can filter by time, workflow ID, workflow name, and workflow status (for example, you can search for all failed workflow executions in the past day).
Click on a workflow to see details, including its input and output:
Click "Show Workflow Steps" to view the workflow's execution as a trace timeline (showing the workflow, its steps, and its child workflows and their steps).
For example, here is the trace of a workflow that processes multiple tasks concurrently by enqueueing child workflows:
You can manage individual workflows directly from the DBOS Console.
##### Cancelling Workflows
You can cancel any workflow that has not completed: `PENDING`, `ENQUEUED`, or `DELAYED`.
Cancelling a workflow sets its status to `CANCELLED`.
If the workflow is currently executing, cancelling it preempts its execution (interrupting it at the beginning of its next step).
If the workflow is enqueued or delayed, cancelling removes it from the queue.
##### Resuming Workflows
You can resume any `ENQUEUED`, `DELAYED`, `CANCELLED` or `MAX_RECOVERY_ATTEMPTS_EXCEEDED` workflow.
Resuming a workflow resumes its execution from its last completed step.
If the workflow is enqueued, this bypasses the queue to start it immediately.
##### Forking Workflows
You can start a new execution of a workflow by **forking** it from a specific step.
To do this, open the workflow steps view, select a particular step, and click "Fork".
When you fork a workflow, DBOS generates a new workflow with a new workflow ID, copies to that workflow the original workflow's inputs and all its steps up to the selected step, then begins executing the new workflow from the selected step.
Forking a workflow is useful for recovering from outages in downstream services (by forking from the step that failed after the outage is resolved) or for "patching" workflows that failed due to a bug in a previous application version (by forking from the bugged step to an application version on which the bug is fixed).
### Exporting Workflows
You can export a workflow from one application to another by clicking the "Export" button in the workflow details panel.
This copies all information on that workflow (and optionally its children) to the other application's system database.
This is most useful for copying workflows from a production to development environment, for example to examine and (using fork) reproduce a bug that originally occurred in production.
---
## DBOS Examples
## Featured Examples
import { FaHackerNews, FaSlack, FaForwardFast, FaPerson } from "react-icons/fa6";
import { HiMiniQueueList } from "react-icons/hi2";
import { BiAddToQueue } from "react-icons/bi";
import { MdOutlineShoppingCart } from "react-icons/md";
import { SiApachekafka } from "react-icons/si";
import { IoEarth } from "react-icons/io5";
import { RiCalendarScheduleLine } from "react-icons/ri";
import { IoIosChatboxes } from "react-icons/io";
import { PiFileMagnifyingGlassBold } from "react-icons/pi";
import { RiCustomerService2Line } from "react-icons/ri";
import { TbClock2 } from "react-icons/tb";
import { VscGraphLine } from "react-icons/vsc";
import { FiInbox } from "react-icons/fi";
---
## Comparing DBOS and Temporal
DBOS and Temporal both provide durable workflows.
The main difference is that Temporal implements durable workflows in a heavyweight orchestration service, whereas DBOS implements them in a Postgres-backed library.
In our opinion, the DBOS architecture is simpler to adopt and operate.
:::info
To learn how to migrate an application from Temporal to DBOS, see the [migration guide](./migrating-from-temporal.md).
:::
### Simpler Architecture
Temporal is designed around a central workflow server that orchestrates workflow execution on a cluster of workers.
The central server runs workflow code, dispatching steps to workers.
Workers execute steps, then return their output to the orchestrator, which durably checkpoints it then dispatches the next step.
Because of this design, adding Temporal to an application requires rearchitecting it.
First, you must move all your workflow and activity (step) code to run on a cluster of Temporal workers.
You must also rewrite all interactions between your application and its workflows to go through the orchestration server and its client APIs.
Then, if you self-host Temporal, you must also operate a highly available Temporal cluster and its supporting datastores (typically a durable database such as Cassandra and a visibility store such as Elasticsearch), effectively adding another distributed system alongside your application.
Alternatively, you can use their managed cloud service, but that places critical workflow state in a third-party service.
In either case, the Temporal server and its data stores are on the critical path for workflow execution and are single points of failure for your system; if they have downtime your application becomes unavailable.
By contrast, DBOS is an open-source Postgres-backed library.
To add DBOS to an application, you install the open-source library and annotate workflows and steps.
The library uses Postgres to checkpoint workflow progress and recover workflows from failure.
Because DBOS uses Postgres for orchestration, you don't need to change how your application is architected or deployed—it can run on any infrastructure connected to any Postgres-compatible database.
You scale DBOS by scaling Postgres, and Postgres [scales well](https://www.dbos.dev/blog/benchmarking-workflow-execution-scalability-on-postgres).
### Advantages of DBOS
#### Improved Operational Reliability
The only point of failure in DBOS is Postgres.
If your organization already uses Postgres, DBOS does not add any new infrastructural dependencies or points of failure to your application's architecture.
By contrast, the Temporal architecture adds multiple new points of failure: the Temporal orchestration server and the datastores it relies on (most commonly Cassandra for durability and Elasticsearch for observability).
Your team is responsible for operating them, and if they have downtime, your application becomes unavailable.
#### >10x Better Latency
In DBOS, the only overhead required to call a step is checkpointing its output.
This requires a single Postgres write, which typically takes 1-2ms.
In Temporal, a step requires an async dispatch from the central server, which takes [tens to hundreds of ms](https://temporal.io/blog/reduce-latency-and-speed-up-your-temporal-workflows).
Thus, DBOS is preferred for interactive or otherwise latency-sensitive workflows.
#### Privacy-Preserving Architecture
Because DBOS stores workflow data in your Postgres database, it is intrinsically privacy-preserving: you own your data, you store it in your Postgres, and it is never stored or sent anywhere else.
By contrast, to use Temporal, you must send potentially sensitive data (including workflow and step checkpoints) to the Temporal server for storage.
#### Rich Workflow Introspection and Management
Because DBOS is built on Postgres, it provides rich SQL-backed workflow introspection and management.
You can search workflows by name, time, queue, version, or custom properties, introspect individual steps, and pause, cancel, or resume workflows.
All these capabilities are available both programmatically and through a web UI.
One particularly powerful and unique feature is **fork**: you can restart a workflow from a specific step, either programmatically or from the UI.
This is useful for recovering from an unexpected failure in a step, such as a failure due to a bug or an outage.
For example, if a large number of billing workflows fail overnight due to an outage in a payment API, you can use fork to restart them all from the payment step after the outage is resolved.
#### Durable Workflow Queues
DBOS provides durable workflow queues with managed flow control.
Using queues, you can manage how many workflows can execute concurrently (globally, per-worker, and per-tenant) as well as which workers can execute which workflows.
Temporal does not have comparable queueing or flow control abstractions, making it harder to control when and where workflows execute.
Learn more about DBOS queues in the queues tutorial ([Python](../python/tutorials/queue-tutorial.md), [TypeScript](../typescript/tutorials/queue-tutorial.md), [Go](../golang/tutorials/queue-tutorial.md), [Java](../java/tutorials/queue-tutorial.md)).
---
## Concurrent Executions
DBOS guarantees that every workflow runs to completion: if an executor crashes or becomes unreachable, another executor recovers its `PENDING` workflows and re-executes them from their last completed step.
The component responsible for recovery, e.g., [DBOS Conductor](../conductor/overview.md), detects unhealthy executors and triggers recovery of its workflows. Sometimes, for example during the rollout of a new application image, that observation can be wrong, and a "zombie" executor could still be running your workflow.
This means the same workflow instance could be running on two executors. (DBOS detects and prevents concurrent executions of the same workflow on the same executor.)
DBOS is designed so that step and workflow invariants are preserved during these situations: steps get at-least-once guarantees and workflow outcomes are persisted exactly-once.
### Workflow Ownership
DBOS detects concurrent executions by tracking which execution **owns** each workflow.
When an execution starts running a workflow (because the workflow was started, dequeued, recovered, or resumed), it generates a unique ownership token, records it in the workflow's row in the [`workflow_status`](./system-tables.md#dbosworkflow_status) table, and keeps it in memory.
Control-plane operations, such as cancelling, resuming, rewinding, or recovering a workflow, clear the recorded token.
Every time an execution writes a checkpoint for the workflow, it first checks, in the same database transaction, that the recorded token still matches its own.
If the token no longer matches, the execution has lost ownership: another execution has taken over the workflow, or it was cancelled.
The checkpoint is not written, and the execution stops running the workflow and **parks**, _i.e._, it waits for the workflow's recorded outcome to become visible in the database, then delivers that recorded outcome through its own handle.
For example, if a "zombie" executor keeps running a workflow after it has been recovered elsewhere, its next checkpoint fails the ownership check, so it stops, and its handle returns the result recorded by the execution that owns the workflow.
Similarly, when you cancel a running workflow, its execution stops at its next checkpoint.
When an execution loses ownership, DBOS throws an exception inside the workflow (`DBOSWorkflowConflictIDError` in Python, `DBOSWorkflowConflictError` in TypeScript, `DBOSWorkflowExecutionConflictException` in Java) or returns an error ([`ErrConflictingWorkflowID`](../golang/reference/workflows-steps.md#errors) in Go).
Do not catch and ignore that error: no subsequent work in the workflow will be made durable.
Separately, if a single execution records a result for a step and then tries to record a different result for the same step, DBOS throws a step nondeterminism error (`DBOSStepNondeterminismError` in Python and TypeScript).
This indicates that the workflow is not deterministic (see determinism requirements in [Python](../python/tutorials/workflow-tutorial.md#determinism) and [TypeScript](../typescript/tutorials/workflow-tutorial.md#determinism)).
---
## DBOSify: Drop-in Temporal Replacement
You can run your existing Temporal code on DBOS with [**DBOSify**](https://github.com/dbos-inc/dbosify-py).
DBOSify is a drop-in replacement for the [Temporal Python SDK](https://github.com/temporalio/sdk-python) that uses Postgres (through [DBOS Transact](https://github.com/dbos-inc/dbos-transact-py)) instead of a Temporal server.
It runs your workflows, activities, signals, updates, queries, retries, and recovery with no infrastructure except Postgres.
To migrate, you import `dbosify` instead of `temporalio` and connect your clients and workers to a Postgres database instead of a Temporal server.
:::info
DBOSify only supports Python for now.
For architectural details and detailed feature compatibility, see the [DBOSify architecture page](https://github.com/dbos-inc/dbosify-py/blob/main/docs/ARCHITECTURE.md).
:::
:::tip
To rewrite a Temporal application to use DBOS natively (in Python, TypeScript, Go, or Java), see [Migrating From Temporal](./migrating-from-temporal.md).
:::
### Using DBOSify
Install DBOSify:
```shell
pip install dbosify
```
Then import `dbosify` instead of `temporalio` and connect to Postgres instead of a Temporal server:
DBOSify Example
```python
import asyncio
import os
from datetime import timedelta
from dbosify import activity, workflow
from dbosify.client import Client
from dbosify.worker import Worker
# A connection string to your Postgres database, instead of a Temporal server address
DB_URL = os.environ.get("DBOS_SYSTEM_DATABASE_URL")
@activity.defn
async def compose_greeting(name: str) -> str:
return f"Hello, {name}!"
@workflow.defn
class GreetingWorkflow:
@workflow.run
async def run(self, name: str) -> str:
return await workflow.execute_activity(
compose_greeting, name, start_to_close_timeout=timedelta(seconds=10)
)
async def main() -> None:
worker = Worker(
DB_URL,
task_queue="greetings",
workflows=[GreetingWorkflow],
activities=[compose_greeting],
)
async with worker:
async with await Client.connect(DB_URL) as client:
result = await client.execute_workflow(
GreetingWorkflow.run, "World", id="greeting-1", task_queue="greetings"
)
print(result) # Hello, World!
if __name__ == "__main__":
asyncio.run(main())
```
### Connection API
Where a Temporal application connects to a Temporal server, a DBOSify application connects to Postgres.
Both the client and the worker take a Postgres connection string.
For more control, you can also construct a client from a `dbos.DBOSClient` and a worker from a `dbos.DBOSConfig`.
**Client:** Connect a client with `Client.connect`:
```python
client = await Client.connect(
system_database_url, # Postgres connection string
namespace="default", # optional; each namespace maps to its own Postgres schema
)
```
For full control, instead build a [`dbos.DBOSClient`](../python/reference/client.md) yourself and pass it to the `Client(...)` constructor.
**Worker:** To configure a `Worker`, pass it a Postgres connection string or a [`dbos.DBOSConfig`](../python/reference/configuration.md):
```python
worker = Worker(
config, # Postgres connection string or a dbos.DBOSConfig
task_queue="greetings", # required
namespace="default", # optional
workflows=[GreetingWorkflow],
activities=[compose_greeting],
)
await worker.run() # or use `async with worker:`
```
---
## Migrating From Temporal
This guide explains how to migrate a Temporal application to DBOS, with a focus on how each major Temporal feature translates to DBOS.
:::info
For a high-level comparison of DBOS and Temporal's architectures, see [Comparing DBOS and Temporal](./comparing-temporal.md).
:::
:::tip
Also check out [DBOSify](./dbosify.md), a drop-in replacement for the Temporal Python SDK backed by Postgres.
:::
### Workflows
The core feature of both DBOS and Temporal is durably executed workflows.
Both DBOS and Temporal automatically recover workflows from the last completed step (activity) after any failure.
Both DBOS and Temporal support extremely long-running workflows, including workflows that run for weeks or months.
**Temporal:**
```python
@workflow.defn
class OrderWorkflow:
@workflow.run
async def run(self, order: Order) -> str:
result = await workflow.execute_activity(
validate_order,
order,
start_to_close_timeout=timedelta(seconds=30),
)
confirmation = await workflow.execute_activity(
process_payment,
result,
start_to_close_timeout=timedelta(seconds=60),
)
return confirmation
```
**DBOS:**
**Python**
```python
@DBOS.workflow()
def order_workflow(order: Order) -> str:
result = validate_order(order)
confirmation = process_payment(result)
return confirmation
```
Learn more in the [workflows tutorial](../python/tutorials/workflow-tutorial.md).
**TypeScript**
```typescript
async function orderWorkflow(order: Order): Promise {
const result = await validateOrder(order);
const confirmation = await processPayment(result);
return confirmation;
}
const orderWorkflowFn = DBOS.registerWorkflow(orderWorkflow);
```
Learn more in the [workflows tutorial](../typescript/tutorials/workflow-tutorial.md).
**Go**
```go
func OrderWorkflow(ctx dbos.DBOSContext, order Order) (string, error) {
result, err := dbos.RunAsStep(ctx, func(stepCtx context.Context) (string, error) {
return validateOrder(stepCtx, order)
}, dbos.WithStepName("validateOrder"))
if err != nil {
return "", err
}
confirmation, err := dbos.RunAsStep(ctx, func(stepCtx context.Context) (string, error) {
return processPayment(stepCtx, result)
}, dbos.WithStepName("processPayment"))
return confirmation, err
}
```
Learn more in the [workflows tutorial](../golang/tutorials/workflow-tutorial.md).
**Java**
```java
@Workflow(name = "orderWorkflow")
public String orderWorkflow(Order order) {
String result = dbos.runStep(() -> validateOrder(order), "validateOrder");
String confirmation = dbos.runStep(() -> processPayment(result), "processPayment");
return confirmation;
}
```
Learn more in the [workflows tutorial](../java/tutorials/workflow-tutorial.md).
#### Starting Workflows
In Temporal, workflows are started through a client connected to the Temporal server. The workflow task is then picked up by a worker, which executes the workflow logic.
In DBOS, workflows can be started directly within your application process. Alternatively, you can enqueue workflows from a separate process using the DBOS Client, which connects directly to the DBOS system database.
**Temporal:**
```python
client = await Client.connect("localhost:7233")
handle = await client.start_workflow(
OrderWorkflow.run,
order,
id="order-123",
task_queue="orders",
)
result = await handle.result()
```
**DBOS:**
**Python**
```python
# Starting a workflow from in your application
with SetWorkflowID("order-123"):
handle = DBOS.start_workflow(order_workflow, order)
result = handle.get_result()
```
```python
# Starting a workflow from another application using the DBOS Client
client = DBOSClient(system_database_url=os.environ["DBOS_SYSTEM_DATABASE_URL"])
handle = client.enqueue({"workflow_name": "order_workflow", "queue_name": "orders"}, order)
result = handle.get_result()
```
Learn more in the [workflows tutorial](../python/tutorials/workflow-tutorial.md).
**TypeScript**
```typescript
// Starting a workflow from in your application
const handle = await DBOS.startWorkflow(orderWorkflowFn, {workflowID: "order-123"})(order);
const result = await handle.getResult();
```
```typescript
// Starting a workflow from another application using the DBOS Client
const client = await DBOSClient.create({systemDatabaseUrl: process.env.DBOS_SYSTEM_DATABASE_URL!});
await client.enqueue(
{ workflowName: "orderWorkflow", queueName: "orders" },
order,
);
```
Learn more in the [workflows tutorial](../typescript/tutorials/workflow-tutorial.md).
**Go**
```go
// Starting a workflow from in your application
handle, err := dbos.RunWorkflow(dbosContext, OrderWorkflow, order, dbos.WithWorkflowID("order-123"))
result, err := handle.GetResult()
```
```go
// Starting a workflow from another application using the DBOS Client
client, err := dbos.NewClient(context.Background(), dbos.ClientConfig{
DatabaseURL: os.Getenv("DBOS_SYSTEM_DATABASE_URL"),
})
handle, err := dbos.Enqueue[Order, string](client, "orders", "OrderWorkflow", order)
result, err := handle.GetResult()
```
Learn more in the [workflows tutorial](../golang/tutorials/workflow-tutorial.md).
**Java**
```java
// Starting a workflow from in your application
WorkflowHandle handle = dbos.startWorkflow(
() -> proxy.orderWorkflow(order),
new StartWorkflowOptions().withWorkflowId("order-123")
);
String result = handle.getResult();
```
```java
// Starting a workflow from another application using the DBOS Client
var client = new DBOSClient(dbUrl, dbUser, dbPassword);
var options = new EnqueueOptions("orderWorkflow", "com.example.OrderImpl", QueueName.of("orders"));
var handle = client.enqueueWorkflow(options, new Object[]{order});
Object result = handle.getResult();
```
Learn more in the [workflows tutorial](../java/tutorials/workflow-tutorial.md).
#### Workflow IDs and Idempotency
Both systems support workflow IDs to provide idempotent execution.
In Temporal, the workflow ID is passed when starting a workflow. In DBOS, you set the workflow ID before invoking the workflow. If a workflow with the same ID has already executed, DBOS returns the previously recorded result instead of running the workflow again.
One important difference is how workflow executions are identified. Temporal uniquely identifies an execution using a combination of workflow ID and run ID, so a workflow may have multiple run instances over time. DBOS, by contrast, treats each execution as uniquely identified by its workflow ID, so a workflow ID corresponds to exactly one execution.
**Python**
```python
with SetWorkflowID("payment-idempotency-key"):
order_workflow(order)
```
Learn more in the [workflows tutorial](../python/tutorials/workflow-tutorial.md#workflow-ids-and-idempotency).
**TypeScript**
```typescript
const handle = await DBOS.startWorkflow(orderWorkflowFn, {workflowID: "payment-idempotency-key"})(order);
```
Learn more in the [workflows tutorial](../typescript/tutorials/workflow-tutorial.md#workflow-ids-and-idempotency).
**Go**
```go
handle, err := dbos.RunWorkflow(dbosContext, OrderWorkflow, order, dbos.WithWorkflowID("payment-idempotency-key"))
```
Learn more in the [workflows tutorial](../golang/tutorials/workflow-tutorial.md#workflow-ids-and-idempotency).
**Java**
```java
dbos.startWorkflow(
() -> proxy.orderWorkflow(order),
new StartWorkflowOptions().withWorkflowId("payment-idempotency-key")
);
```
Learn more in the [workflows tutorial](../java/tutorials/workflow-tutorial.md#workflow-ids-and-idempotency).
#### Determinism
Both DBOS and Temporal require workflows to be deterministic. Non-deterministic operations (API calls, random numbers, current time) must happen inside activities/steps, not directly in the workflow function.
#### Durable Timers
Temporal's `workflow.sleep()` maps directly to `DBOS.sleep()`. Both are durable and persist across restarts.
**Temporal:**
```python
await workflow.sleep(timedelta(hours=24))
```
**DBOS:**
**Python**
```python
DBOS.sleep(86400) # seconds
```
Learn more in the [workflows tutorial](../python/tutorials/workflow-tutorial.md#durable-sleep).
**TypeScript**
```typescript
await DBOS.sleep(86400000); // milliseconds
```
Learn more in the [workflows tutorial](../typescript/tutorials/workflow-tutorial.md#durable-sleep).
**Go**
```go
dbos.Sleep(ctx, 24 * time.Hour)
```
Learn more in the [workflows tutorial](../golang/tutorials/workflow-tutorial.md#durable-sleep).
**Java**
```java
dbos.sleep(Duration.ofHours(24));
```
Learn more in the [workflows tutorial](../java/tutorials/workflow-tutorial.md#durable-sleep).
#### Continue-as-New
A common pattern in Temporal is to use an extremely long-running workflow as a durable object.
Applications interact with it via signals and queries and periodically refresh its state with `continue_as_new` to avoid Temporal's workflow size limits.
In DBOS, there are no workflow history limits beyond the underlying database column and storage limits. However, instead of maintaining extremely long-lived workflows, which can slow down replay during recovery, we generally recommend storing long-lived objects directly in your database and interacting with them through shorter-lived workflows.
To coordinate those interactions, you can use DBOS durable queues, especially partitioned queues, to control concurrency and ordering. This approach provides similar guarantees while avoiding the complexity of managing extremely long-running workflows.
If you have workflows with many steps, another useful pattern is to use an outer control workflow that orchestrates smaller sub-workflows (child workflows). This improves observability (because you can easily isolate each sub-workflow) and can speed up recovery and replay.
### Activities → Steps
Temporal activities map to DBOS steps. Both are where side effects and non-deterministic operations happen.
The key architectural difference is how they are executed. In Temporal, activities are dispatched to workers, often running in separate processes, through the Temporal server. This introduces a network round trip between the workflow and the worker executing the activity. Temporal also supports local activities that run in the same process as the workflow, but they come with several limitations.
In DBOS, steps run in the same process as the workflow and are invoked like regular function calls. DBOS automatically checkpoints the step's result to your database, guaranteeing durability without requiring a separate worker process. Because execution happens in place, steps typically have lower latency and less overhead compared to remotely dispatched activities.
**Temporal:**
```python
@activity.defn
async def send_email(to: str, body: str) -> bool:
response = requests.post(EMAIL_API, json={"to": to, "body": body})
return response.ok
```
**DBOS:**
**Python**
```python
@DBOS.step()
def send_email(to: str, body: str) -> bool:
response = requests.post(EMAIL_API, json={"to": to, "body": body})
return response.ok
```
Learn more in the [steps tutorial](../python/tutorials/step-tutorial.md).
**TypeScript**
```typescript
const sendEmail = DBOS.registerStep(async (to: string, body: string): Promise => {
const response = await fetch(EMAIL_API, {
method: "POST",
body: JSON.stringify({ to, body }),
});
return response.ok;
});
```
Learn more in the [steps tutorial](../typescript/tutorials/step-tutorial.md).
**Go**
```go
// Steps are called inline using RunAsStep
result, err := dbos.RunAsStep(ctx, func(stepCtx context.Context) (bool, error) {
return sendEmail(stepCtx, to, body)
}, dbos.WithStepName("sendEmail"))
```
Learn more in the [steps tutorial](../golang/tutorials/step-tutorial.md).
**Java**
```java
// Steps are called inline using dbos.runStep
boolean result = dbos.runStep(() -> sendEmail(to, body), "sendEmail");
```
Learn more in the [steps tutorial](../java/tutorials/step-tutorial.md).
#### Retries
Both systems support configurable retries with exponential backoff.
**Temporal**:
```python
result = await workflow.execute_activity(
send_email,
args=[to, body],
start_to_close_timeout=timedelta(seconds=30),
retry_policy=RetryPolicy(
initial_interval=timedelta(seconds=1),
backoff_coefficient=2.0,
maximum_attempts=5,
),
)
```
**DBOS:**
**Python**
```python
@DBOS.step(retries_allowed=True, max_attempts=5, interval_seconds=1.0, backoff_rate=2.0)
def send_email(to: str, body: str) -> bool:
response = requests.post(EMAIL_API, json={"to": to, "body": body})
return response.ok
```
Learn more in the [steps tutorial](../python/tutorials/step-tutorial.md#configurable-retries).
**TypeScript**
```typescript
const sendEmail = DBOS.registerStep(
async (to: string, body: string): Promise => {
const response = await fetch(EMAIL_API, {
method: "POST",
body: JSON.stringify({ to, body }),
});
return response.ok;
},
{ retriesAllowed: true, maxAttempts: 5, intervalSeconds: 1.0, backoffRate: 2.0 }
);
```
Learn more in the [steps tutorial](../typescript/tutorials/step-tutorial.md#configurable-retries).
**Go**
```go
result, err := dbos.RunAsStep(ctx, func(stepCtx context.Context) (bool, error) {
return sendEmail(stepCtx, to, body)
},
dbos.WithStepName("sendEmail"),
dbos.WithStepMaxRetries(5),
dbos.WithBaseInterval(1 * time.Second),
dbos.WithBackoffFactor(2.0),
)
```
Learn more in the [steps tutorial](../golang/tutorials/step-tutorial.md#configurable-retries).
**Java**
```java
boolean result = dbos.runStep(
() -> sendEmail(to, body),
new StepOptions("sendEmail")
.withMaxAttempts(5)
.withRetryInterval(Duration.ofSeconds(1))
.withBackoffRate(2.0)
);
```
Learn more in the [steps tutorial](../java/tutorials/step-tutorial.md#configurable-retries).
#### Heartbeats
Temporal activities support heartbeats for long-running operations so the server knows the activity is still alive. DBOS does not require heartbeats because there is no central orchestrator monitoring activity execution; instead, steps run directly in your application process.
#### Database Operations
DBOS provides a special type of step called a transaction that executes database operations in a single database transaction, co-committed with the DBOS checkpoint.
This provides exactly-once semantics for database writes, which is stronger than the at-least-once semantics offered by Temporal.
**Python**
Learn more in the [transactions tutorial](../python/tutorials/transaction-tutorial.md).
```python
import os
from dbos import SQLAlchemyDatasource
from sqlalchemy import text
ds = SQLAlchemyDatasource.create(os.environ["APP_DATABASE_URL"])
@ds.transaction()
def update_order_status(order_id: str, status: str) -> None:
ds.sql_session().execute(
text("UPDATE orders SET status = :status WHERE id = :id"),
{"status": status, "id": order_id}
)
```
**TypeScript**
Learn more in the [transactions tutorial](../typescript/tutorials/transaction-tutorial.md).
```typescript
const dataSource = new KnexDataSource('app-db', {
client: 'pg', connection: process.env.DBOS_DATABASE_URL
});
async function updateOrderStatus(orderId: string, status: string) {
await dataSource.client('orders')
.where({ id: orderId })
.update({ status });
}
const updateOrderStatusTx = dataSource.registerTransaction(updateOrderStatus);
```
### Signals → Messages
Temporal signals allow external processes to send data to a running workflow. In DBOS, the equivalent mechanism is **messages (notifications)**, which external processes send using `send()` and workflows read using `recv()`.
**Temporal:**
```python
# In the workflow
@workflow.defn
class OrderWorkflow:
def __init__(self):
self.payment_status = None
@workflow.signal
async def payment_received(self, status: str):
self.payment_status = status
@workflow.run
async def run(self, order: Order):
# ... start order processing ...
await workflow.wait_condition(lambda: self.payment_status is not None)
if self.payment_status == "paid":
# handle success
else:
# handle failure
# Sending the signal
handle = client.get_workflow_handle("order-123")
await handle.signal(OrderWorkflow.payment_received, "paid")
```
**DBOS:**
**Python**
```python
# In the workflow
@DBOS.workflow()
def order_workflow(order: Order):
# ... start order processing ...
payment_status = DBOS.recv("payment_status", timeout_seconds=3600)
if payment_status is not None and payment_status == "paid":
# handle success
else:
# handle failure
# Sending the message
DBOS.send("order-123", "paid", topic="payment_status")
```
Learn more in the [workflow communication tutorial](../python/tutorials/workflow-communication.md#workflow-messaging-and-notifications).
**TypeScript**
```typescript
// In the workflow
async function orderWorkflow(order: Order) {
// ... start order processing ...
const paymentStatus = await DBOS.recv("payment_status", 3600);
if (paymentStatus !== null && paymentStatus === "paid") {
// handle success
} else {
// handle failure
}
}
const orderWorkflowFn = DBOS.registerWorkflow(orderWorkflow);
// Sending the message
await DBOS.send("order-123", "paid", "payment_status");
```
Learn more in the [workflow communication tutorial](../typescript/tutorials/workflow-communication.md#workflow-messaging-and-notifications).
**Go**
```go
// In the workflow
func OrderWorkflow(ctx dbos.DBOSContext, order Order) (string, error) {
// ... start order processing ...
paymentStatus, err := dbos.Recv[string](ctx, "payment_status", 1*time.Hour)
if err != nil {
return "", err
}
if paymentStatus == "paid" {
// handle success
} else {
// handle failure
}
// ...
}
// Sending the message
err := dbos.Send(dbosContext, "order-123", "paid", "payment_status")
```
Learn more in the [workflow communication tutorial](../golang/tutorials/workflow-communication.md#workflow-messaging-and-notifications).
**Java**
```java
// In the workflow
@Workflow(name = "orderWorkflow")
public void orderWorkflow(Order order) {
// ... start order processing ...
Optional paymentStatus = dbos.recv("payment_status", Duration.ofHours(1));
if (paymentStatus.map("paid"::equals).orElse(false)) {
// handle success
} else {
// handle failure
}
}
// Sending the message
dbos.send("order-123", "paid", "payment_status");
```
Learn more in the [workflow communication tutorial](../java/tutorials/workflow-communication.md#workflow-messaging-and-notifications).
Messages are persisted to the database, so they remain available even after the workflow completes.
### Queries → Events
Temporal queries allow external code to synchronously read the state of a workflow.
In DBOS, the equivalent mechanism is **events**, which workflows publish using `set_event()` and external processes read using `get_event()`.
**Temporal:**
```python
@workflow.defn
class OrderWorkflow:
def __init__(self):
self.progress = 0
@workflow.query
def get_progress(self) -> int:
return self.progress
@workflow.run
async def run(self, order: Order):
self.progress = 25
await workflow.execute_activity(validate_order, order, ...)
self.progress = 50
# ...
# Querying workflow state
handle = client.get_workflow_handle("order-123")
progress = await handle.query(OrderWorkflow.get_progress)
```
**DBOS:**
**Python**
```python
@DBOS.workflow()
def order_workflow(order: Order):
DBOS.set_event("progress", 25)
validate_order(order)
DBOS.set_event("progress", 50)
# ...
# Reading workflow state
progress = DBOS.get_event("order-123", "progress")
```
Learn more in the [workflow communication tutorial](../python/tutorials/workflow-communication.md#workflow-events).
**TypeScript**
```typescript
async function orderWorkflow(order: Order) {
await DBOS.setEvent("progress", 25);
await validateOrder(order);
await DBOS.setEvent("progress", 50);
// ...
}
const orderWorkflowFn = DBOS.registerWorkflow(orderWorkflow);
// Reading workflow state
const progress = await DBOS.getEvent("order-123", "progress");
```
Learn more in the [workflow communication tutorial](../typescript/tutorials/workflow-communication.md#workflow-events).
**Go**
```go
func OrderWorkflow(ctx dbos.DBOSContext, order Order) (string, error) {
dbos.SetEvent(ctx, "progress", 25)
// ... validate order ...
dbos.SetEvent(ctx, "progress", 50)
// ...
}
// Reading workflow state
progress, err := dbos.GetEvent[int](dbosContext, "order-123", "progress", 30*time.Second)
```
Learn more in the [workflow communication tutorial](../golang/tutorials/workflow-communication.md#workflow-events).
**Java**
```java
@Workflow(name = "orderWorkflow")
public void orderWorkflow(Order order) {
dbos.setEvent("progress", 25);
// ... validate order ...
dbos.setEvent("progress", 50);
// ...
}
// Reading workflow state
Optional progress = dbos.getEvent("order-123", "progress", Duration.ofSeconds(30));
```
Learn more in the [workflow communication tutorial](../java/tutorials/workflow-communication.md#workflow-events).
Events are persisted to the database, so they remain available even after the workflow completes.
### Task Queues → Queues
Temporal task queues control which workers execute which workflows. DBOS queues serve a similar purpose but also provide built-in advanced concurrency control and rate limiting.
**Temporal:**
```python
# Worker listens to a task queue
worker = Worker(
client,
task_queue="order-processing",
workflows=[OrderWorkflow],
activities=[validate_order, process_payment],
)
await worker.run()
# Start workflow on a specific queue
handle = await client.start_workflow(
OrderWorkflow.run, order, id="order-123", task_queue="order-processing"
)
```
**DBOS:**
**Python**
```python
# Register a queue with concurrency limits
DBOS.register_queue("order-processing", global_concurrency=10)
# Enqueue a workflow
handle = DBOS.enqueue_workflow("order-processing", order_workflow, order)
result = handle.get_result()
```
Learn more in the [queues tutorial](../python/tutorials/queue-tutorial.md).
**TypeScript**
```typescript
// Register a queue with concurrency limits
await DBOS.registerQueue("order-processing", { globalConcurrency: 10 });
// Enqueue a workflow
const handle = await DBOS.startWorkflow(orderWorkflowFn, { queueName: "order-processing" })(order);
const result = await handle.getResult();
```
Learn more in the [queues tutorial](../typescript/tutorials/queue-tutorial.md).
**Go**
```go
// Define a queue with concurrency limits
queue := dbos.NewWorkflowQueue(dbosContext, "order-processing", dbos.WithGlobalConcurrency(10))
// Enqueue a workflow
handle, err := dbos.RunWorkflow(dbosContext, OrderWorkflow, order, dbos.WithQueue(queue.Name))
result, err := handle.GetResult()
```
Learn more in the [queues tutorial](../golang/tutorials/queue-tutorial.md).
**Java**
```java
// Register a queue with concurrency limits (after dbos.launch())
dbos.registerQueue("order-processing", QueueOptions.setConcurrency(10));
// Enqueue a workflow
WorkflowHandle handle = dbos.startWorkflow(
() -> proxy.orderWorkflow(order),
new StartWorkflowOptions().withQueue("order-processing")
);
String result = handle.getResult();
```
Learn more in the [queues tutorial](../java/tutorials/queue-tutorial.md).
DBOS queues provide features that Temporal task queues don't have out of the box:
- **Global concurrency limits**: Limit total concurrent executions across all workers.
- **Per-worker concurrency**: Limit concurrent executions per process.
- **Global rate limiting**: Limit executions per time period across all workers.
- **Partitioned queues**: Create per-tenant sub-queues with independent concurrency limits.
- **Priority**: Process higher-priority workflows first.
- **Deduplication**: Prevent duplicate workflows in the queue.
- **Debouncing**: Delay a workflow's execution until some time has passed since it was last called.
### Scheduled Workflows
Both DBOS and Temporal let you run workflows on a cron schedule:
**Temporal:**
```python
await client.create_schedule(
"daily-report",
Schedule(
action=ScheduleActionStartWorkflow(
DailyReportWorkflow.run,
id="daily-report",
task_queue="reports",
),
spec=ScheduleSpec(cron_expressions=["0 9 * * *"]),
),
)
```
**DBOS:**
**Python**
```python
DBOS.create_schedule(
schedule_name="daily-report",
workflow_fn=daily_report_workflow,
schedule="0 9 * * *",
)
```
DBOS schedules also support pausing, resuming, backfilling missed runs, and triggering immediate execution.
Learn more in the [scheduling tutorial](../python/tutorials/scheduled-workflows.md).
**TypeScript**
```typescript
await DBOS.createSchedule({
scheduleName: "daily-report",
workflowFn: dailyReportWorkflow,
schedule: "0 9 * * *",
});
```
DBOS schedules also support pausing, resuming, backfilling missed runs, and triggering immediate execution.
Learn more in the [scheduling tutorial](../typescript/tutorials/scheduled-workflows.md).
### Child Workflows
Both Temporal and DBOS support calling a child workflow from within another workflow.
**Temporal:**
```python
@workflow.defn
class ParentWorkflow:
@workflow.run
async def run(self):
result = await workflow.execute_child_workflow(
ChildWorkflow.run, args=[data]
)
```
**DBOS:**
**Python**
```python
@DBOS.workflow()
def parent_workflow():
# Call directly (runs inline)
result = child_workflow(data)
# Or start in background
handle = DBOS.start_workflow(child_workflow, data)
result = handle.get_result()
```
Learn more in the [workflows tutorial](../python/tutorials/workflow-tutorial.md#starting-workflows-in-the-background).
**TypeScript**
```typescript
async function parentWorkflow() {
// Call directly (runs inline)
const result = await childWorkflowFn(data);
// Or start in background
const handle = await DBOS.startWorkflow(childWorkflowFn)(data);
const result2 = await handle.getResult();
}
const parentWorkflowFn = DBOS.registerWorkflow(parentWorkflow);
```
Learn more in the [workflows tutorial](../typescript/tutorials/workflow-tutorial.md#starting-workflows-in-the-background).
**Go**
```go
func ParentWorkflow(ctx dbos.DBOSContext, input string) (string, error) {
// Start child workflow in background
handle, err := dbos.RunWorkflow(ctx, ChildWorkflow, data)
if err != nil {
return "", err
}
result, err := handle.GetResult()
return result, err
}
```
Learn more in the [workflows tutorial](../golang/tutorials/workflow-tutorial.md).
**Java**
```java
@Workflow(name = "parentWorkflow")
public String parentWorkflow() {
// Call directly (runs inline)
String result = proxy.childWorkflow(data);
// Or start in background
WorkflowHandle handle = dbos.startWorkflow(
() -> proxy.childWorkflow(data),
new StartWorkflowOptions()
);
return handle.getResult();
}
```
Learn more in the [workflows tutorial](../java/tutorials/workflow-tutorial.md#starting-workflows-in-the-background).
### Codecs and Encryption
In Temporal, you can define a codec to encrypt workflow information before it is stored on a Temporal server to limit Temporal's access to sensitive data.
In DBOS, this is rarely necessary because data is stored **only** in your own database.
However, if it is necessary to store sensitive data encrypted, you can use a custom serializer ([Python](../python/reference/contexts.md#custom-serialization), [TypeScript](../typescript/reference/configuration.md#custom-serialization)) to encrypt your data before storing it and decrypt it before retrieving it.
### What's Different in DBOS
#### No Orchestration Server
DBOS has no central server to manage, operate, or scale. Your workflows run in your application process and checkpoint directly to your database. This eliminates a major source of operational complexity and latency.
#### Fork
DBOS can [fork a workflow](../python/tutorials/workflow-management.md) from a specific step, re-executing it from that point. This is powerful for recovering from failures, for example, restarting thousands of failed workflows from a specific step after an outage is resolved.
#### Workflow Streaming
DBOS provides [streaming](../python/tutorials/workflow-communication.md#workflow-streaming), an append-only stream that workflows can write to and clients can read from in real time. This is useful for streaming LLM outputs, progress updates, or real-time data from long-running workflows.
#### Database Integration & SQL-Based Introspection
DBOS integrates deeply with your database.
For example, you can [enqueue workflows directly from Postgres PL/pgSQL function](./portable-workflows.md#per-workflow-enqueue).
You can also use [transactional steps](#database-operations) to perform database operations in workflows with exactly-once semantics.
Moreover, because all workflow state is stored in your database, you can query it with SQL.
DBOS also provides programmatic APIs to [list, search, and manage workflows](../python/tutorials/workflow-management.md) by status, name, time, queue, or custom properties.
#### Queue Flow Control
Using DBOS queues, you can manage how many workflows can execute concurrently (globally, per-worker, and per-tenant) as well as which workers can execute which workflows.
Temporal does not have comparable queueing or flow control abstractions, making it harder to control when and where workflows execute.
### Automating Temporal -> DBOS Migration
With coding agents, you can largely automate a migration from Temporal to DBOS.
To do this, we recommend using DBOS skills and prompts to give your coding agent access to the latest information on DBOS:
- [AI-assisted development in Python](../python/prompting.md)
- [AI-assisted development in TypeScript](../typescript/prompting.md)
- [AI-assisted development in Go](../golang/prompting.md)
- [AI-assisted development in Java](../java/prompting.md)
---
## Cross-Language Interaction
DBOS supports multiple languages—Python, TypeScript, Go, and Java—each with its own SDK.
A client in one language can connect to the [system database](./system-tables.md) of an application written in another language to exchange data through workflows, messages, events, and streams.
Applications in different languages can also [share a single system database](./sharing-a-system-database.md).
However, each language has a native serialization format that the other languages can't read.
The **portable JSON** serialization format solves this by providing a common data representation that all SDKs can read and write, and can even be read and written from the database without any DBOS code at all.
### Default Serialization Is Language-Specific
By default, each DBOS SDK serializes data using its language's default format.
These default formats are chosen for their fidelity to the wide range of data structures and objects available in each language:
| Language | Default Format | Format Name |
|------------|---------------------|-----------------|
| Python | pickle | `py_pickle` |
| TypeScript | SuperJSON | `js_superjson` |
| Go | encoding/json | `DBOS_JSON` |
| Java | Jackson | `java_jackson` |
As the set of data structures and classes varies from language to language, data written in one language's default format cannot be read by the other languages.
For example, a Python workflow that writes an event using pickle produces a binary blob that TypeScript and Java can't deserialize.
### Portable JSON Format
The `portable_json` format is straightforward use of JSON that all SDKs can read and write.
While a smaller subset of language constructs can be serialized, any DBOS application in any language can read or write it.
**Supported types:**
- JSON primitives: `null`, booleans, numbers, and strings
- JSON arrays (ordered lists of JSON values)
- JSON objects (maps with strings as keys and JSON values)
**Type conversions:**
Some language built-in and library types are mapped to equivalent JSON constructs.
When these values are decoded, the recipient must restore them to the appropriate language equivalent.
- Date/time values are converted to [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339) UTC strings (e.g., `"2025-06-15T14:30:00.000Z"`)
| Language | Type | Portable Representation |
|------------|------------------------|-------------------------|
| Python | `datetime` | RFC 3339 UTC string |
| Python | `date` | ISO 8601 string |
| Python | `Decimal` | Numeric string |
| Python | `set`, `tuple` | JSON array |
| Go | `time.Time` | RFC 3339 UTC string |
| Java | `Instant` | RFC 3339 UTC string |
| Java | `BigDecimal` | Numeric string |
| TypeScript | `Date` | RFC 3339 UTC string |
| TypeScript | `BigInt` | Numeric string |
| TypeScript | `Map` (string keys) | JSON object |
| TypeScript | `Set` | JSON array |
### Using Portable Serialization
You can opt in to portable serialization at the workflow or operation level.
Workflows started with portable serialization return their results or exceptions in portable format.
Workflows started with portable serialization also write their events and streams in portable JSON by default, but this can be overridden for each operation.
#### Per-Workflow (Enqueue)
When enqueuing or starting a workflow from a `DBOSClient`, or when enqueueing a workflow to another application [sharing the same system database](./sharing-a-system-database.md), set the serialization format in the enqueue options.
This ensures the workflow's arguments are serialized in portable format that can be read by the target language.
If multiple applications [share the system database](./sharing-a-system-database.md), also name the application that owns the workflow, so that application runs it.
You can also enqueue a workflow using the PL/pgSQL function [`dbos.enqueue_workflow`](system-tables.md#dbosenqueue_workflow).
Only portable serialization is allowed when enqueuing using PL/pgSQL.
**Python**
```python
from dbos import DBOSClient, WorkflowSerializationFormat
client = DBOSClient(
system_database_url=db_url,
# The name of the application that implements process_order
application_name="order-service",
)
handle = client.enqueue(
{
"workflow_name": "process_order",
"queue_name": "orders",
"serialization_type": WorkflowSerializationFormat.PORTABLE,
},
"order-123",
)
```
**TypeScript**
```typescript
import { DBOSClient } from "@dbos-inc/dbos-sdk";
const client = await DBOSClient.create({
systemDatabaseUrl: process.env.DBOS_SYSTEM_DATABASE_URL!,
// The name of the application that implements process_order
applicationName: "order-service",
});
const handle = await client.enqueue(
{
workflowName: "process_order",
queueName: "orders",
serializationType: "portable",
},
"order-123",
);
```
**Java**
```java
import dev.dbos.transact.DBOSClient;
import dev.dbos.transact.EnqueueOptions;
import dev.dbos.transact.workflow.QueueName;
import dev.dbos.transact.workflow.SerializationStrategy;
var client = new DBOSClient(dbUrl, dbUser, dbPassword);
var options = new EnqueueOptions("process_order", QueueName.of("orders"))
// The name of the application that implements process_order
.withApplicationName("order-service")
.withSerialization(SerializationStrategy.PORTABLE);
var handle = client.enqueueWorkflow(options, new Object[] {"order-123"});
```
**Go**
```go
import "github.com/dbos-inc/dbos-transact-golang/dbos"
client, _ := dbos.NewClient(context.Background(), dbos.ClientConfig{
DatabaseURL: os.Getenv("DBOS_SYSTEM_DATABASE_URL"),
// The name of the application that implements process_order
AppName: "order-service",
})
// In Go, use dbos.PortableWorkflowArgs to request a portable enqueue
args := dbos.PortableWorkflowArgs{
PositionalArgs: []any{"order-123"},
}
handle, err := dbos.Enqueue[any](
client, "orders", "process_order", args,
)
```
**PL/pgSQL**
```sql
DECLARE workflow_id text;
workflow_id := dbos.enqueue_workflow(
workflow_name => 'processOrder',
class_name => 'com.example.OrderProcessor',
queue_name => 'orders',
positional_args => ARRAY['"order-123"'::json]
);
```
#### Per-Workflow (via Annotation or Decorator)
You can set the serialization strategy directly on the workflow annotation or decorator so that the workflow uses portable serialization by default when started:
**Python**
```python
from dbos import DBOS, WorkflowSerializationFormat
@DBOS.workflow(serialization_type=WorkflowSerializationFormat.PORTABLE)
def process_order(order_id: str):
# All inputs, outputs, events, and streams for this workflow
# use portable JSON serialization by default
return f"processed: {order_id}"
```
**TypeScript**
Using a decorator:
```typescript
import { DBOS } from "@dbos-inc/dbos-sdk";
export class Orders {
@DBOS.workflow({ serialization: "portable" })
static async processOrder(orderId: string): Promise {
// All inputs, outputs, events, and streams for this workflow
// use portable JSON serialization by default
return `processed: ${orderId}`;
}
}
```
Or using `registerWorkflow`:
```typescript
async function processOrder(orderId: string): Promise {
return `processed: ${orderId}`;
}
const processOrderWorkflow = DBOS.registerWorkflow(processOrder, {
name: "processOrder",
serialization: "portable",
});
```
**Go**
In Go, portable serialization is set per-invocation using the `WithPortableWorkflow` option on `RunWorkflow`:
```go
handle, err := dbos.RunWorkflow(dbosContext, processOrder, "order-123",
dbos.WithPortableWorkflow(),
)
```
**Java**
```java
import dev.dbos.transact.workflow.SerializationStrategy;
import dev.dbos.transact.workflow.Workflow;
@Workflow(serializationStrategy = SerializationStrategy.PORTABLE)
public String processOrder(String orderId) {
// All inputs, outputs, events, and streams for this workflow
// use portable JSON serialization by default
return "processed: " + orderId;
}
```
:::note
The default serialization strategy only affects invocations that are aware of the annotation / decorator. This makes the default useful for unit testing, but the actual serialization strategy used will depend on how the workflow is enqueued by the client.
:::
#### For Workflow Communication
Setting the serialization format at the workflow level affects the default for `setEvent` and `writeStream`.
However, individual operations can override this—for example, a workflow running with native serialization may want to publish a specific event in portable format for cross-language consumption, or a portable workflow may need to record an event with the greater flexibility afforded by the native serializer.
Each language's `setEvent` and `writeStream` methods accept a serialization parameter for this purpose.
`send` is a special case, because messages target a different workflow and the sender does not know what serialization that workflow expects.
In every language, a `send` from inside a workflow defaults to that workflow's serialization format.
You should therefore always set the serialization format explicitly on `send` when communicating cross-language.
You can also send a message to a workflow using the PL/pgSQL function [`dbos.send_message`](system-tables.md#dbossend_message).
Only portable serialization is allowed when sending a message using PL/pgSQL.
Note, there is no PL/pgSQL version of `setEvent` or `writeStream`.
:::info
Step outputs always use the native serializer regardless of the workflow's serialization strategy.
Steps are internal to a workflow and are not read by other languages, so the native serializer's greater flexibility is preferred.
:::
**Python**
```python
from dbos import DBOS, WorkflowSerializationFormat
# Send a message readable by any language
DBOS.send(
destination_id="workflow-123",
message={"status": "complete", "count": 42},
topic="updates",
serialization_type=WorkflowSerializationFormat.PORTABLE,
)
# Set an event readable by any language
DBOS.set_event(
"progress",
{"percent": 75},
serialization_type=WorkflowSerializationFormat.PORTABLE,
)
# Write to a stream readable by any language
DBOS.write_stream(
"results",
{"item": "processed"},
serialization_type=WorkflowSerializationFormat.PORTABLE,
)
```
**TypeScript**
```typescript
import { DBOS } from "@dbos-inc/dbos-sdk";
// Send a message readable by any language
await DBOS.send(
"workflow-123",
{ status: "complete", count: 42 },
"updates",
undefined, // idempotencyKey
{ serializationType: "portable" }
);
// Set an event readable by any language
await DBOS.setEvent(
"progress",
{ percent: 75 },
{ serializationType: "portable" }
);
// Write to a stream readable by any language
await DBOS.writeStream(
"results",
{ item: "processed" },
{ serializationType: "portable" }
);
```
**Go**
```go
import "github.com/dbos-inc/dbos-transact-golang/dbos"
// Send a message readable by any language
dbos.Send(ctx, "workflow-123",
map[string]any{"status": "complete", "count": 42},
"updates",
dbos.WithPortableSend(),
)
// Set an event readable by any language
dbos.SetEvent(ctx, "progress",
map[string]any{"percent": 75},
dbos.WithPortableSetEvent(),
)
// Write to a stream readable by any language
dbos.WriteStream(ctx, "results",
map[string]any{"item": "processed"},
dbos.WithPortableWriteStream(),
)
```
**Java**
```java
import dev.dbos.transact.DBOS;
import dev.dbos.transact.workflow.SerializationStrategy;
// Send a message readable by any language
dbos.send(
"workflow-123",
Map.of("status", "complete", "count", 42),
"updates",
null, // idempotencyKey
SerializationStrategy.PORTABLE
);
// Set an event readable by any language
dbos.setEvent(
"progress",
Map.of("percent", 75),
SerializationStrategy.PORTABLE
);
```
**PL/pgSQL**
```sql
PERFORM dbos.send_message(
destination_id => 'workflow-123',
message => '{"status": "complete", "count": 42}'::json,
topic => 'updates'
);
```
### Portable Errors
When a workflow using portable serialization fails, its error is serialized in a standard JSON structure that all languages can inspect:
```json
{
"name": "ValueError",
"message": "Order not found",
"code": 404,
"data": {"orderId": "order-123"}
}
```
| Field | Type | Description |
|-----------|----------------------|----------------------------------------|
| `name` | string | The error type/class name |
| `message` | string | Human-readable error message |
| `code` | number, string, null | Optional application-specific error code |
| `data` | any JSON value, null | Optional structured error details |
#### Raising Portable Errors
You can explicitly raise a portable error from a workflow:
**Python**
```python
from dbos import PortableWorkflowError
raise PortableWorkflowError(
message="Order not found",
name="NotFoundError",
code=404,
data={"orderId": "order-123"},
)
```
**TypeScript**
```typescript
import { PortableWorkflowError } from "@dbos-inc/dbos-sdk";
throw new PortableWorkflowError(
"Order not found",
"NotFoundError",
404,
{ orderId: "order-123" },
);
```
**Go**
```go
import "github.com/dbos-inc/dbos-transact-golang/dbos"
return nil, &dbos.PortableWorkflowError{
Name: "NotFoundError",
Message: "Order not found",
Code: 404,
Data: map[string]any{"orderId": "order-123"},
}
```
**Java**
```java
import dev.dbos.transact.json.PortableWorkflowException;
throw new PortableWorkflowException(
"Order not found",
"NotFoundError",
404,
Map.of("orderId", "order-123")
);
```
#### Reading Portable Errors
When a workflow that used portable serialization fails, other languages receive the error as a `PortableWorkflowError` (Python/TS/Go) or `PortableWorkflowException` (Java) with the `name`, `message`, `code`, and `data` fields populated.
If a workflow fails with a non-portable exception while using portable serialization, DBOS automatically converts it to the portable error format on a best-effort basis, extracting the error type name, message, and any common error code attributes.
### Input Validation and Coercion
When a workflow is started via portable JSON—whether from another language, a `DBOSClient`, or a direct database insert—the arguments arrive as plain JSON values.
JSON has a limited type system: numbers are untyped (no distinction between `int`, `long`, `double`, or other language-specific offerings), there is no native date type (dates arrive as strings), and collection types may not match the target language's expectations (e.g., a JSON array becomes a generic `ArrayList` in Java, not a typed list or object array).
Each SDK provides a way to validate these arguments so that the workflow function receives the types it expects.
Note that while workflow argument validation is possible, return values, messages, and events are not automatically coerced, as the expected types are not known at runtime. These must be validated and coerced manually.
Each SDK's approach is documented in its language-specific reference:
- **[Java — Automatic Coercion](../java/reference/workflows-steps.md#input-validation-and-coercion)**: Java automatically coerces portable JSON arguments to match the workflow method's parameter types (e.g., `Integer` → `long`, ISO-8601 strings → `Instant`). No opt-in required.
- **[TypeScript — Input Schema (Zod)](../typescript/reference/workflows-steps.md#input-validation-and-coercion)**: TypeScript workflows can specify an `inputSchema` (compatible with [Zod](https://zod.dev/)) that validates and optionally transforms arguments before the workflow runs.
- **Go — Automatic Coercion**: Go automatically coerces portable JSON arguments to match the workflow function's parameter types using type assertion. No opt-in required.
- **[Python — Argument Validator (Pydantic)](../python/reference/decorators.md#input-validation-and-coercion)**: Python workflows can specify `validate_args=pydantic_args_validator` to validate arguments against the function's type hints using [Pydantic](https://docs.pydantic.dev/).
### Further Reading
- **Serialization strategy reference:**
- [Python Serialization Strategy](../python/reference/contexts.md#serialization-strategy)
- [TypeScript Serialization Strategy](../typescript/reference/methods.md#serialization-strategy)
- [Go Portable Options](../golang/reference/methods.md#portable-serialization-options-and-types)
- [Java Serialization Strategy](../java/reference/methods.md#serialization-strategy)
- **Custom serialization configuration:**
- [Python Custom Serialization](../python/reference/contexts.md#custom-serialization)
- [TypeScript Custom Serialization](../typescript/reference/configuration.md#custom-serialization)
- [Java Custom Serialization](../java/reference/lifecycle.md#custom-serialization)
- **System tables:** The [`serialization` column](./system-tables.md) in system tables records which format was used for each piece of serialized data.
---
## Sharing a System Database
Multiple DBOS applications, potentially in different languages, can share a single system database.
Each application is identified by its configured name and owns everything it creates: workflows, steps, queues, schedules, and application versions.
Applications sharing a system database are isolated from one another by default, but can freely interoperate by naming each other.
For example, one application can enqueue another's workflows and wait for their results.
### Application Names and Ownership
Every application is identified by the `name` in its configuration, so each application sharing a system database must have a distinct name.
Ownership determines which application runs what:
- A workflow is dequeued, run, and recovered only by the application that owns it.
- A queue is polled only by the application that registered it, even if another application enqueues workflows on it.
- A schedule is fired only by the application that created it, and its workflows are owned by that application.
- Application versions are tracked per application, so one application's deployments do not affect which version its peers consider latest.
[Retention policies](../conductor/retention.md) are an exception: their time and rows thresholds apply to the entire system database, including workflows owned by other applications. The global timeout remains scoped to the application that configures it.
Queue, schedule, and version names remain globally unique across all applications sharing a system database; registering a name that a different application already owns raises an error.
Workflow IDs are also unique across the entire system database, so ID-addressed operations (retrieving a workflow's handle, status, or result by ID, and sending messages or reading events and streams) work across applications regardless of ownership.
Observability queries (`list_workflows`, `list_queues`, `list_schedules`) are scoped to the calling application by default.
### Calling Another Application's Workflows
To run another application's workflow, enqueue it by name, naming the application that implements it.
The enqueued workflow is owned by the target application, which dequeues and runs it on its latest application version.
Because workflow IDs are global, you can then wait for the result from the returned handle.
**Python**
```python
from dbos import DBOS, EnqueueOptions
options: EnqueueOptions = {
"workflow_name": "process_order",
"queue_name": "orders",
# The name of the application that implements process_order
"application_name": "order-service",
}
handle = DBOS.enqueue_workflow_with_options(options, "order-123")
result = handle.get_result()
```
**TypeScript**
```typescript
const handle = await DBOS.enqueueWorkflowWithOptions({
workflowName: "process_order",
queueName: "orders",
// The name of the application that implements process_order
applicationName: "order-service",
}, "order-123");
const result = await handle.getResult();
```
**Go**
```go
handle, err := dbos.Enqueue[any](ctx, "orders", "process_order",
"order-123",
// The name of the application that implements process_order
dbos.WithEnqueueApplicationName("order-service"),
)
if err != nil {
return err
}
result, err := handle.GetResult()
```
**Java**
```java
var options = new EnqueueOptions("process_order", QueueName.of("orders"))
// The name of the application that implements process_order
.withApplicationName("order-service");
WorkflowHandle