Skip to content

Building Your Own Environment with OpenEnv

This guide walks you through creating a custom environment using the OpenEnv framework and the openenv CLI.

The CLI handles scaffolding, builds, validation, and deployment so you can stay focused on environment logic.

Overview

A typical workflow looks like:

  1. Scaffold a new environment with openenv init.
  2. Customize your models, environment logic, and FastAPI server.
  3. Implement a typed HTTPEnvClient.
  4. Configure dependencies and the Dockerfile once.
  5. Use the CLI (openenv build, openenv validate, openenv push) to package and share your work.

Note

These integrations are handled automatically by the openenv CLI when you run openenv init.

Prerequisites

  • Python 3.11+ and uv for dependency locking
  • Docker Desktop / Docker Engine
  • The OpenEnv library installed: pip install https://github.com/meta-pytorch/OpenEnv.git

Step-by-Step Guide

Let's walk through the process of building a custom environment with OpenEnv.

1. Scaffold with openenv init

# Run from anywhere – defaults to current directory
openenv init my_env

# Optionally choose an output directory
openenv init my_env --output-dir /Users/you/src/envs

The command creates a fully-typed template with openenv.yaml, pyproject.toml, uv.lock, Docker assets, and stub implementations. If you're working inside this repo, move the generated folder under src/envs/.

Typical layout:

my_env/
├── __init__.py
├── README.md
├── client.py
├── models.py
├── openenv.yaml
├── pyproject.toml
├── uv.lock
└── server/
    ├── __init__.py
    ├── app.py
    ├── my_environment.py
    ├── requirements.txt
    └── Dockerfile

Python classes are generated for the action, observation, and state, and a client is generated for the environment. For example, you will find MyEnvironment, MyAction, MyObservation, and MyState in the my_env directory based on the name of the environment you provided.

2. Define Models

Edit models.py to describe your action, observation, and state dataclasses:

# models.py
from dataclasses import dataclass
from core.env_server import Action, Observation, State

@dataclass
class MyAction(Action):
    """Your custom action."""
    command: str
    parameters: dict

@dataclass
class MyObservation(Observation):
    """Your custom observation."""
    result: str
    success: bool

@dataclass
class MyState(State):
    """Custom state fields."""
    custom_field: int = 0

3. Implement Environment Logic

Customize server/my_environment.py by extending Environment:

# server/my_environment.py
import uuid
from core.env_server import Environment
from ..models import MyAction, MyObservation, MyState

class MyEnvironment(Environment):
    def __init__(self):
        super().__init__()
        self._state = MyState()

    def reset(self) -> MyObservation:
        self._state = MyState(episode_id=str(uuid.uuid4()))
        return MyObservation(result="Ready", success=True)

    def step(self, action: MyAction) -> MyObservation:
        # Implement your logic here
        self._state.step_count += 1
        result = self._execute_command(action.command)
        return MyObservation(result=result, success=True)

    @property
    def state(self) -> MyState:
        return self._state

4. Create the FastAPI Server

server/app.py should expose the environment through create_fastapi_app:

# server/app.py
from core.env_server import create_fastapi_app
from ..models import MyAction, MyObservation
from .my_environment import MyEnvironment

env = MyEnvironment()
app = create_fastapi_app(env, MyAction, MyObservation)

5. Implement the Client

client.py extends HTTPEnvClient so users can interact with your server over HTTP or Docker:

# client.py
from core.http_env_client import HTTPEnvClient
from core.types import StepResult
from .models import MyAction, MyObservation, MyState

class MyEnv(HTTPEnvClient[MyAction, MyObservation]):
    def _step_payload(self, action: MyAction) -> dict:
        return {"command": action.command, "parameters": action.parameters}

    def _parse_result(self, payload: dict) -> StepResult[MyObservation]:
        obs = MyObservation(**payload["observation"])
        return StepResult(
            observation=obs,
            reward=payload.get("reward"),
            done=payload.get("done", False),
        )

    def _parse_state(self, payload: dict) -> MyState:
        return MyState(**payload)

6. Configure Dependencies & Dockerfile

The CLI template ships with pyproject.toml and server/Dockerfile. You should manage your python dependencies with uv or pip in the pyproject.toml file. Other dependencies should be installed in the Dockerfile.

Keep building from the openenv-base image so shared tooling stays available:

# Accept base image as build argument for CI/CD flexibility
ARG BASE_IMAGE=ghcr.io/meta-pytorch/openenv-base:latest
FROM ${BASE_IMAGE}

# Install dependencies
COPY src/envs/my_env/server/requirements.txt /tmp/requirements.txt
RUN pip install --no-cache-dir -r /tmp/requirements.txt && rm /tmp/requirements.txt

# Copy environment code
COPY src/core/ /app/src/core/
COPY src/envs/my_env/ /app/src/envs/my_env/

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD curl -f http://localhost:8000/health || exit 1

# Run server
CMD ["uvicorn", "envs.my_env.server.app:app", "--host", "0.0.0.0", "--port", "8000"]

If you introduced extra dependencies in the Dockerfile, you should install them in the Dockerfile before removing temp files.

7. Build & Validate with the CLI

From the environment directory:

cd src/envs/my_env
openenv build          # Builds Docker image (auto-detects context)
openenv validate --verbose

openenv build understands both standalone environments and in-repo ones. Useful flags:

  • --tag/-t: override the default openenv-<env_name> tag
  • --build-arg KEY=VALUE: pass multiple Docker build arguments
  • --dockerfile / --context: custom locations when experimenting
  • --no-cache: force fresh dependency installs

openenv validate checks for required files, ensures the Dockerfile/server entrypoints function, and lists supported deployment modes. The command exits non-zero if issues are found so you can wire it into CI.

8. Push & Share with openenv push

Once validation passes, the CLI can deploy directly to Hugging Face Spaces or any registry:

# Push to HF Spaces (auto enables web UI and prompts for login if needed)
openenv push

# Push to a specific repo or namespace
openenv push --repo-id my-org/my-env

# Push to Docker/ghcr (interface disabled by default)
openenv push --registry ghcr.io/my-org --tag my-env:latest

# Customize image base or visibility
openenv push --base-image ghcr.io/meta-pytorch/openenv-base:latest --private

Key options:

  • --directory: path to the environment (defaults to cwd)
  • --repo-id: explicit Hugging Face space name
  • --registry: push to Docker Hub, GHCR, etc.
  • --interface/--no-interface: toggle the optional web UI
  • --base-image: override the Dockerfile FROM
  • --private: mark the space as private

The command validates your openenv.yaml, injects Hugging Face frontmatter when needed, and uploads the prepared bundle.

9. Automate Builds (optional)

To trigger Docker builds on every push to main, add your environment to the matrix in .github/workflows/docker-build.yml:

strategy:
  matrix:
    image:
      - name: echo-env
        dockerfile: src/envs/echo_env/server/Dockerfile
      - name: chat-env
        dockerfile: src/envs/chat_env/server/Dockerfile
      - name: coding-env
        dockerfile: src/envs/coding_env/server/Dockerfile
      - name: my-env  # Add your environment here
        dockerfile: src/envs/my_env/server/Dockerfile

Use Your Environment

For an end-to-end example of using your environment, see the Quick Start guide. Here is a simple example of using your environment:

from envs.my_env import MyAction, MyEnv

# Create environment from Docker image
client = MyEnv.from_docker_image("my-env:latest")
# Or, connect to the remote space on Hugging Face
client = MyEnv.from_hub("my-org/my-env")
# Or, connect to the local server
client = MyEnv(base_url="http://localhost:8000")

# Reset
result = client.reset()
print(result.observation.result)  # "Ready"

# Execute actions
result = client.step(MyAction(command="test", parameters={}))
print(result.observation.result)
print(result.observation.success)

# Get state
state = client.state()
print(state.episode_id)
print(state.step_count)

# Cleanup
client.close()

Nice work! You've now built and used your own OpenEnv environment.

Your next steps are to: