How to Run a CrewAI Agent on DigitalOcean Harness Runtimepublic

Last verified 13 Oct 2026

DigitalOcean Harness Runtime provides managed, hardware-isolated microVM sandboxes with built-in tools such as Chromium to run harnesses and execute arbitrary code. Rich lifecycle APIs preserve conversational history and working state across sessions, letting you pause, resume, and fork work to control costs and adapt to the nonlinear nature of agentic workflows. Scale complete agents such as Claude Code or use sandboxes independently for code execution, all through the same service. See What You Can Build for example use cases.

DigitalOcean Harness Runtime runs your CrewAI application in an isolated sandbox. Unlike coding adapters, the CrewAI runtime runs your crew, rather than a general-purpose coding agent. The image includes only the mediator that runs your crew.

At session start, Harness Runtime does the following:

  1. Clones your repository at an exact commit, named in the crewai.framework block of your spec, into the sandbox workspace.
  2. Installs your Python dependencies from that checkout.
  3. Imports your Crew object into harness_crewai, a small FastAPI mediator that the platform hosts for you.
  4. Drives the mediator over HTTP/SSE. Each turn’s prompt text and structured inputs become one Crew.kickoff(inputs=...) call, and the results stream back to you.

Your application stays a standard CrewAI app. You do not need platform-specific imports or an SDK. To use the Action Gateway tool catalog, the platform can pass your crew a standard MCP endpoint in an environment variable, which your crew attaches with CrewAI’s own MCP support. You can also create an Action Gateway session from your crew with the pydo client.

The CrewAI runtime uses the following components:

Component Value
Runtime adapter crewai
Sandbox template crewai
Mediator harness_crewai, a FastAPI process that imports your crew and drives kickoff()
Python version in the image Python 3.12 (Ubuntu 24.04)
Pinned CrewAI version crewai==1.15.11
Session idle timeout 600 seconds (10 minutes) between turns

To run a CrewAI agent, you need a GitHub repository that contains your CrewAI app, with crewai.json at the repository root (or in a subdirectory you point to) and the exact commit SHA you want to run.

Note

CrewAI has no asynchronous human-in-the-loop. Task(human_input=True) is a blocking console prompt and hangs a headless sandbox, so crews that use it are rejected at preflight. If your workflow needs an approval step mid-run, this adapter does not support it yet.

Set Up the Repository

A deployable CrewAI app needs a constructed Crew instance, a way for the platform to find it, and a dependency file. Unlike LangGraph, there is no separate compile step. We support both of the following layouts:

my-crew/
|-- e2e_crew/
|   |-- __init__.py
|   |-- crew.py
|   |-- agents.yaml
|   `-- tasks.yaml
|-- requirements.txt
`-- crewai.json
my-crew/
|-- e2e_crew/
|   |-- __init__.py
|   |-- crew.py
|   |-- agents.yaml
|   `-- tasks.yaml
|-- pyproject.toml
`-- crewai.json

agents.yaml and tasks.yaml are optional CrewAI convention files. crewai.json (or crewai.jsonc, or a [tool.crewai] table in pyproject.toml) must resolve at the repository root, or in the directory you set with crewai.framework.subdir for monorepos. You can skip the manifest by setting crewai.framework.object in your spec.

Create the crewai.json Manifest

Create crewai.json at the root of your repository. This file tells the harness_crewai mediator where your Crew object lives and, optionally, how a submitted prompt maps onto kickoff inputs.

Minimal example:

{
  "crew": "e2e_crew.crew:crew"
}

With an input mapping:

{
  "crew": "e2e_crew.crew:crew",
  "inputs": {
    "prompt_key": "topic",
    "defaults": {
      "year": 2026
    }
  }
}

The manifest supports the following fields:

  • crew (required unless you set crewai.framework.object or a [tool.crewai] table): Points at your Crew instance as module:attr. The attribute must already be a constructed Crew(...), not a factory function.
  • inputs.prompt_key (optional): Kickoff-input name for the submitted prompt text. Without it, a plain-text turn is passed as {"input": "<text>"}.
  • inputs.defaults (optional): Values merged under each turn’s inputs so tasks that expect fixed keys receive them on every run.

For each turn, inputs merge in this order: defaults, then the turn’s structured inputs, then {prompt_key: <submitted text>}. Later layers overwrite earlier keys, and non-empty text always wins for the prompt key.

Note

Declare the input mapping in your spec instead. The crewai.inputs block in the spec carries the same prompt_key and defaults, plus a typed schema that every turn is validated against. When the spec declares crewai.inputs, it takes precedence over the inputs block in crewai.json and is the only one checked at submit. If you use the spec block, keep crewai.json minimal with the crew key only. See Declare Crew Inputs.

Warning

Do not commit real secrets to a .env file in your repository. Put credentials under secrets and non-secret config under env, then read them with os.environ in your Python code.

Declare Dependencies

At boot, we install the following dependencies from your checked-out repository:

  • requirements.txt: pip install --no-cache-dir -r requirements.txt
  • pyproject.toml: pip install --no-cache-dir -e .
  • Neither: No install step. Your crew can only use packages preinstalled in the image.

If both requirements.txt and pyproject.toml exist, requirements.txt takes precedence and pyproject.toml is ignored. Pick one to avoid surprises.

For a typical LLM-backed crew, use a requirements.txt similar to:

crewai>=0.80
crewai-tools[mcp]>=0.60

The [mcp] extra is only needed if you attach Action Gateway tools through the session endpoint. If you use the PyDo client instead, add pydo>=0.41.0. See Connect Action Gateway Tools.

Expose a Crew Instance

Point the crew key at a constructed Crew instance. Create or open e2e_crew/crew.py. Placeholders such as {topic} in agent and task text are filled from the turn’s kickoff inputs:

import os

from crewai import Agent, Crew, Process, Task, LLM

llm = LLM(
    model=os.environ["HARNESS_INFERENCE_MODEL"],
    base_url=os.environ["HARNESS_INFERENCE_BASE_URL"],
    api_key=os.environ["HARNESS_INFERENCE_API_KEY"],
)

researcher = Agent(
    role="Researcher",
    goal="Find accurate, current information on {topic}",
    backstory="A meticulous researcher who cites sources.",
    llm=llm,
)

research_task = Task(
    description="Research {topic} for {audience} and list {max_facts} key facts.",
    expected_output="A concise, well-sourced summary.",
    agent=researcher,
)

crew = Crew(
    agents=[researcher],
    tasks=[research_task],
    process=Process.sequential,
)

The mediator sets crew.stream = True when your crew does not already set it, so token and tool events appear live in doctl harness-runtime launch and the Console. Do not set human_input=True on any Task. A crew with any such task fails mediator preflight, before your session can boot.

Write the Environment Spec

Create a file such as crewai-agent.yaml that names your repository and commit. The clone target lives in the crewai.framework block:

name: my-crewai-agent
agent: crewai
template: crewai
size: mars-2vcpu-4gb
persistent_workspace: true
egress:
  - inference.do-ai.run
crewai:
  framework:
    repo: https://github.com/<owner>/<repo>.git
    repo_sha: <exact-40-character-commit-sha>
    # subdir: services/research
    # object: research_crew.crew:crew
env:
  HARNESS_INFERENCE_BASE_URL: https://inference.do-ai.run/v1
  HARNESS_INFERENCE_MODEL: <your-model-name>
secrets:
  HARNESS_INFERENCE_API_KEY: ${HARNESS_INFERENCE_API_KEY}

Export the inference API key so doctl can expand ${HARNESS_INFERENCE_API_KEY}:

export HARNESS_INFERENCE_API_KEY=<your-inference-api-key>

The spec uses the following fields:

  • agent (required): Must be crewai.
  • template (optional): crewai selects the CrewAI guest image and is the default for agent: crewai.
  • crewai.framework.repo (required): Full https://github.com/... URL of your crew repository. HTTP URLs and non-GitHub hosts are rejected.
  • crewai.framework.repo_sha (required): Exact 40-character commit SHA to check out. Branch and tag names are rejected because they move.
  • crewai.framework.subdir (optional): Repository-relative directory that contains crewai.json, for monorepos. The value must not contain ...
  • crewai.framework.object (optional): module:attr naming your crew. It bypasses crewai.json entirely and applies to the crewai adapter only.
  • crewai.inputs (optional): The crew’s kickoff inputs: prompt key, defaults, and a typed schema. See Declare Crew Inputs.
  • size (optional): A sandbox size slug (mars-1vcpu-1gb, mars-2vcpu-2gb, mars-2vcpu-4gb, mars-4vcpu-8gb, or mars-16vcpu-32gb).
  • env and secrets (optional): Non-secret configuration under env and credentials under secrets. See Connect a Model.
  • egress (optional): Hosts the sandbox may reach, such as your model endpoint. When absent, egress is unrestricted.
  • tools and permissions (optional): Action Gateway tools and their allow rules, if you use the session endpoint option. See Connect Action Gateway Tools.
Note

Always pin crewai.framework.repo_sha to an exact commit so every session boots the same code and your deployments stay reproducible.

Warning

Declare every credential under secrets. Credential-shaped values under env are rejected.

Migrate from env.FRAMEWORK Fields

Older specs set the clone target as env.FRAMEWORK_REPO, FRAMEWORK_REPO_SHA, FRAMEWORK_SUBDIR, and FRAMEWORK_OBJECT. That spelling still works during the transition. The server lifts it into the crewai.framework block, the session boots unchanged, and create returns one deprecation warning per key. doctl harness-runtime validate accepts it too.

Declaring a value both ways with different contents is rejected with a 400 error, for example when env.FRAMEWORK_REPO_SHA and crewai.framework.repo_sha differ. Move to the block when you can, because the env will be removed in the future. The same applies to env.FRAMEWORK_INPUT_PROMPT_KEY and FRAMEWORK_INPUT_DEFAULTS, which crewai.inputs.prompt_key and crewai.inputs.defaults replace.

A top-level framework: or inputs: block is rejected. Nest both under crewai:. The Kubernetes-style apiVersion, kind, and spec envelope still parses but is deprecated.

Declare Crew Inputs

A CrewAI turn is a Crew.kickoff(inputs={...}) call. The optional crewai.inputs block in your spec declares what that dictionary may contain, so the platform can fill it from a turn and reject a bad turn before it reaches your crew.

crewai:
  inputs:
    prompt_key: topic
    defaults:
      audience: engineers
      year: 2026
      max_facts: 3
      min_confidence: 0.8
      include_sources: true
    schema:
      topic:           {type: string,  required: true}
      audience:        {type: string}
      year:            {type: integer, required: true}
      max_facts:       {type: integer}
      min_confidence:  {type: number}
      include_sources: {type: boolean}

The block supports the following fields:

  • prompt_key: The input that a turn’s free text fills. Required when schema is set, and it must name a declared string input.
  • defaults: Values merged under every turn. With a schema, each default must be declared and match its type. Values are strings, numbers, or true and false values. Lists, objects, and null are not accepted.
  • schema.<name>.type: One of string, integer, number, or boolean.
  • schema.<name>.required: The turn is rejected unless the input ends up set by the turn’s inputs, a default, or, for the prompt key, the turn’s text.

Input names are CrewAI placeholder names: letters, digits, underscores, and hyphens, not starting with a digit or hyphen, with a maximum of 64 bytes. The schema holds at most 64 inputs.

Understand Turn Validation

With a schema declared, every SendInput turn, whether from the Console, doctl, a trigger, or the API, is validated at submit. A turn that fails gets a 400 error that names the input, and no run is created.

Turn Result
Text how coral reefs form, no inputs Runs. topic comes from the text and everything else from defaults.
Inputs {"topic": "rust ownership", "audience": "students", "max_facts": 2} Runs, with those three values overriding the defaults.
Inputs {"topc": "x"} 400 invalid inputs["topc"]: not declared in the agent's inputs.schema (did you mean "topic"?)
Inputs {"topic": "x", "year": "2026"} 400 invalid inputs["year"]: declared integer, got string
Inputs {"topic": "x", "max_facts": 2.5} 400 invalid inputs["max_facts"]: declared integer, got 2.5
A required input with no default that the turn does not set 400 invalid inputs["year"]: required by the agent's inputs.schema; send it in inputs

Integers must have an absolute value of at most 9007199254740991 (2^53 minus 1). Declare larger values as string. Without a schema, prompt_key and defaults still apply, and turns are only held to a general shape: a flat object of strings (up to 16 KiB each), finite numbers, and true and false values, with at most 64 keys.

Fix Create-Time Errors

Mistakes in the declaration itself fail at session or config create. For example:

  • crewai.inputs.prompt_key is required with crewai.inputs.schema: A turn’s text must fill a declared input.
  • crewai.inputs.schema.tags.type "list" must be one of string, integer, number, boolean: The type is not supported.
  • crewai.inputs.defaults.yr is not declared in crewai.inputs.schema: Every default needs a matching schema entry.
  • inputs is only read by the crewai adapter: The block appears under a different agent.
  • Declaring crewai.inputs together with env.FRAMEWORK_INPUT_PROMPT_KEY or FRAMEWORK_INPUT_DEFAULTS is rejected. Declare the mapping once, in crewai.inputs.
Note

A value sent in a turn’s inputs currently reaches your crew as a float, so 2 renders as 2.0 in task text. This happens because the turn is carried as JSON numbers end to end. Defaults declared in the spec are unaffected, so year: 2026 renders as 2026. Until a fix ships, declare an input as string if its exact text matters.

Connect a Model

Your crew owns model configuration. We do not choose or inject a model client for you. Declare your settings in the spec, then read them from os.environ in Python.

Use the following environment variables:

  • HARNESS_INFERENCE_API_KEY: API key for your inference endpoint. Declare it under secrets.
  • HARNESS_INFERENCE_BASE_URL: OpenAI-compatible base URL, including the /v1 suffix, for example https://inference.do-ai.run/v1.
  • HARNESS_INFERENCE_MODEL: Model name to request from that endpoint.

CrewAI’s LLM class takes model, base_url, and api_key directly. Create the client once at module scope so every agent reuses it:

import os

from crewai import LLM

llm = LLM(
    model=os.environ["HARNESS_INFERENCE_MODEL"],
    base_url=os.environ["HARNESS_INFERENCE_BASE_URL"],
    api_key=os.environ["HARNESS_INFERENCE_API_KEY"],
    temperature=0,
)

We recommend the following:

  • Never hardcode keys in your repository, and keep them out of env. Values in env are readable by anyone who can inspect the session.
  • Read configuration with os.environ["KEY"] rather than a silent default, so missing configuration fails at import time.
  • Other variable names work too. HARNESS_INFERENCE_* is the platform convention.

Access a Private Repository

If crewai.framework.repo is private, declare GITHUB_TOKEN under secrets. The token is sent as an HTTP Authorization header on the fetch. It is never written into the repository URL or a git remote, and only the pinned commit is fetched (--depth 1).

crewai:
  framework:
    repo: https://github.com/<owner>/<private-repo>.git
    repo_sha: <exact-40-character-commit-sha>
secrets:
  GITHUB_TOKEN:
    source: oauth
    provider: github
    actor: <actor-id>

Create the GitHub connection first with doctl harness-runtime connections create --actor <actor-id>, and use the same actor ID in the spec. You can also declare a personal access token instead. A fine-grained token with Contents: Read on the repository is enough. Classic tokens with the repo scope and GitHub App installation tokens also work. See Connect GitHub.

Connect Action Gateway Tools

Action Gateway gives your crew governed access to a catalog of hosted tools, such as web search, web fetch, and code execution. Coding adapters attach these tools automatically. CrewAI has no automatic MCP attach, so your crew code wires the tools in itself. You can choose between the following two options:

Session endpoint PyDo client
How it works The platform mints a session-pinned Action Gateway endpoint and passes its URL to your crew in an environment variable. Your crew attaches it with CrewAI’s MCP support. Your crew creates its own Action Gateway session with pydo and wraps the discovered tools as CrewAI BaseTools.
Credential in the sandbox None. The endpoint URL is pinned to your session and needs no token. A DigitalOcean API token (DIGITALOCEAN_TOKEN) under secrets.
Where tool policy lives In the spec, under tools and permissions. In your crew code, in the session.create(permissions=...) call.
Extra egress None for Action Gateway. api.digitalocean.com and actions.do-ai.run.
Dependency crewai-tools[mcp] pydo>=0.41.0

Choose the session endpoint if you want the sandbox to hold no Action Gateway credential. Choose the PyDo client if you prefer to keep policy in code and accept that the sandbox holds a token scoped for Action Gateway.

Use the Session Endpoint

Select the Tools in Your Spec

Add the tools to tools, preload the ones your crew calls by name, and allow them under permissions:

tools:
  - do.actions:
      tools: [exa_web_search]
      preload_tools: [exa_web_search]
permissions:
  rules:
    - tool: do.actions/exa_web_search
      action: allow

tools limits what the session may reach. preload_tools is what makes a tool appear by name to your crew. Without it, your crew sees only the gateway’s three meta tools (action_search, action_invoke, and action_code) and never calls exa_web_search directly. Every preload_tools entry must also appear in tools. Keep the list short, because each preloaded schema sits in the model’s context on every turn.

Tool slugs are provider-qualified, for example exa_web_search and not web_search. A bare slug is rejected at create. See the environment spec reference for the full tools syntax.

Read the Endpoint from the Environment

Every MCP server your spec attaches gets its own environment variable in the sandbox, which holds that server’s endpoint URL:

In your spec Environment variable
- do.actions HARNESS_MCP_URL_DO_ACTIONS
Inline server named internal.jira HARNESS_MCP_URL_INTERNAL_JIRA

The name is HARNESS_MCP_URL_ followed by the server name in upper case, with . and - turned into _. The value is only the URL. The Action Gateway endpoint is pinned to your session and needs no credential. The prefix is reserved, so env cannot set these variables.

Attach the Tools in Your Crew Code

Add CrewAI’s tools package with its MCP extra to your dependencies (crewai-tools[mcp]), then pass the URL to MCPServerAdapter. Open e2e_crew/crew.py and add the following:

import os

from crewai import Agent
from crewai_tools import MCPServerAdapter

ag_url = os.environ.get("HARNESS_MCP_URL_DO_ACTIONS")
ag_tools = (
    MCPServerAdapter(
        {"url": ag_url, "transport": "streamable-http"},
        "exa_web_search",
    ).tools
    if ag_url
    else []
)

researcher = Agent(
    role="Researcher",
    goal="Gather 2-3 crisp facts about {topic}",
    backstory="You are a concise researcher.",
    llm=llm,
    tools=ag_tools,
)

Tools keep the names the gateway advertises. The tool names after the server parameters narrow the result. Leave them out to get every tool the gateway lists. The empty list when ag_url is unset lets the same code run locally, outside Managed Agents. The adapter connects when your module is imported at session boot and waits up to 30 seconds by default (connect_timeout).

If you use @CrewBase, set mcp_server_params on the class and pick tools per agent with get_mcp_tools(). CrewAI opens the connection when it first needs the tools and closes it after each kickoff:

ag_url = os.environ.get("HARNESS_MCP_URL_DO_ACTIONS")


@CrewBase
class ResearchCrew:
    mcp_server_params = (
        {"url": ag_url, "transport": "streamable-http"} if ag_url else None
    )

    @agent
    def researcher(self) -> Agent:
        return Agent(
            config=self.agents_config["researcher"],
            tools=self.get_mcp_tools("exa_web_search"),
        )
Warning

Do not use Agent(mcps=[MCPServerHTTP(url=...)]) for gateway tools. CrewAI prefixes every tool name with the endpoint’s host and path, then cuts it to 64 characters. With a session endpoint, the model sees only names that end in a hash. Use MCPServerAdapter or @CrewBase as shown.

Enforcement Limits for Crews

A crew runs your code directly. No platform permission layer sits in front of it, and nothing in it can show an approval prompt. Only what is enforced outside the sandbox applies:

In your spec Enforced
egress Yes, at the network layer.
do.actions/<tool> rules with allow or deny Yes, by Action Gateway.
ask on a do.actions tool (a rule or default: ask) No. The call goes through without asking. Use allow or deny.
Any other rule (bash, file.*, web.fetch, or an inline <server>/<tool>) No. By defaults, a strict deny fails session create because it cannot be enforced.
An inline server’s tools selection No. Your crew sees the server’s full tool list.

Session create warns about each “No” in this table. If your crew needs one of them, enforce it in your code.

Attach Your Own MCP Servers

An inline server declared under tools gets its own HARNESS_MCP_URL_<NAME> variable. Its credential is not in the URL. Read it from the environment variable named after its auth.secret slot and send it yourself, for example {"url": ..., "transport": "streamable-http", "headers": {"Authorization": "Bearer " + token}}. List the server’s host under egress.

Use the PyDo Client

With this option, your crew mints its own Action Gateway session and the platform does not attach a tool endpoint. Because the crew holds a real token, treat DIGITALOCEAN_TOKEN like any other credential, and scope it as narrowly as your token flow allows.

Declare the Token and Egress

Provide a DigitalOcean personal access token authorized for Action Gateway under secrets as DIGITALOCEAN_TOKEN. Never put it under env, because env values are readable by anyone who can inspect the session. This flow talks to the public DigitalOcean API and Action Gateway hosts directly, so allow egress to both. Do not add tools or permissions.rules entries for Action Gateway. Policy lives in your code:

secrets:
  HARNESS_INFERENCE_API_KEY: ${HARNESS_INFERENCE_API_KEY}
  DIGITALOCEAN_TOKEN: ${DIGITALOCEAN_TOKEN}
egress:
  - inference.do-ai.run
  - api.digitalocean.com
  - actions.do-ai.run

Export the token so doctl can expand ${DIGITALOCEAN_TOKEN}, and add pydo>=0.41.0 to your dependency file:

export DIGITALOCEAN_TOKEN=<your-digitalocean-api-token>

Create the Session and Wrap Tools in Your Crew Code

Start a session that names the tools your crew needs, then wrap each discovered tool as a CrewAI BaseTool. Call session.create once at import time, not per kickoff, so each turn does not create a new Action Gateway session:

import os


def load_action_gateway_tools() -> list:
    token = os.environ.get("DIGITALOCEAN_TOKEN", "")
    if not token:
        return []

    requested = ["exa_web_search"]

    from pydo.action_gateway import ActionGatewayClient

    client = ActionGatewayClient(token=token)
    session = client.session.create(
        actor_id=os.environ.get("SESSION_ID", "my-crew"),
        tools=requested,
        config={"preloadTools": requested},
        permissions={
            "default_action": "deny",
            "rules": [
                {"tool": name, "action": "allow"} for name in requested
            ],
        },
    )

    from crewai.tools import BaseTool
    from pydantic import create_model

    tools = []
    for tool_def in session.tools.list(include_all=True):
        name = tool_def.get("name")
        if name not in requested:
            continue
        schema = tool_def.get("inputSchema") or {}
        props = schema.get("properties", {})
        required = set(schema.get("required", []))
        fields = {
            p: (str, ... if p in required else None) for p in props
        }
        args_model = (
            create_model(f"{name}_Args", **fields) if fields else None
        )

        class _GatewayTool(BaseTool):
            def _run(self, **kwargs):
                return session.tools.call(name, kwargs)

        kwargs = {
            "name": name,
            "description": tool_def.get("description", ""),
        }
        if args_model is not None:
            kwargs["args_schema"] = args_model
        tools.append(_GatewayTool(**kwargs))

    return tools


# Attach when you construct agents:
# researcher = Agent(..., tools=load_action_gateway_tools())

session.tools.call(name, arguments) invokes one catalog tool and returns its output. For tool discovery, parallel calls, and model tool-call loops, see the pydo/examples/gateway examples in the PyDo repository.

Note

Your session.create(permissions=...) call is the only Action Gateway policy gate in this option. There is no platform-side approval prompt, so declare an explicit allow rule for every tool your crew calls. Tool-call events show the real tool name, such as exa_web_search, rather than a generic gateway envelope.

Deploy and Run the Crew

Check the spec client-side:

doctl harness-runtime validate --spec ./crewai-agent.yaml

Create a session from the spec:

doctl harness-runtime create --spec ./crewai-agent.yaml

Cold start clones your repository, installs dependencies, and runs preflight checks on your crew before the session is ready. Note the session ID, then attach to the session in your terminal:

doctl harness-runtime launch <session-id-or-name>

Detach with Ctrl+D. The session stays alive until you pause or remove it. To send one headless turn, run:

doctl harness-runtime prompt <session-id-or-name> "how coral reefs form"

To replay the event stream, run:

doctl harness-runtime logs <session-id-or-name>

Send Structured Inputs

launch and prompt send prompt text only, which fills prompt_key. To set other inputs for a turn, call the session’s input endpoint:

curl -sS -X POST "https://api.digitalocean.com/v2/agents/sessions/<session-id>/input" \
  -H "Authorization: Bearer $DIGITALOCEAN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"text": "", "inputs": {"topic": "rust ownership", "audience": "students", "max_facts": 2}}'

The request returns {"run_id": "..."} right away, and the run’s output arrives on the event stream.

Read the Event Stream

Each turn produces an ordered stream of events mapped from CrewAI’s event bus:

Event Meaning
run.started Your input started a run, which is a fresh Crew.kickoff() call. The data.agent field is a JSON string of the merged kickoff inputs, including the prompt text under your prompt_key.
token Reply text streamed live as your crew’s agents produce it.
run.tool_call_started / run.tool_call_completed A CrewAI tool call started or finished, sourced from CrewAI’s own event bus.
run.completed The crew finished. Token usage and the model are reported here.
run.failed The run ended with an error. The message carries the reason.

Each turn is an independent Crew.kickoff() call against the same long-running mediator process. Nothing carries conversation context from one turn to the next unless your crew configures memory itself.

Remove the session when you are finished:

doctl harness-runtime remove <session-id-or-name>

Session names must be unique within your team. Remove the previous session or change name before reusing a name. The CrewAI guest idle timeout is 10 minutes between turns. Send input within that window, or start a new session after an idle timeout ends the run.

Check Your Setup Before Deploying

Before you deploy, confirm the following:

  • crewai.json exists at your repository root or in crewai.framework.subdir, unless you set crewai.framework.object.
  • The crew key points at an already-constructed Crew instance in module:attr form.
  • requirements.txt or pyproject.toml lists every package your crew imports, and the crew imports cleanly against them. Mediator preflight rejects the session if imports fail.
  • No Task sets human_input=True.
  • Your commit is pushed to GitHub, and crewai.framework.repo and repo_sha name it exactly.
  • agent: crewai is set, plus template: crewai if you set a template.
  • If your crew uses placeholders beyond the prompt, crewai.inputs declares them, with defaults for any it needs on every turn, and prompt_key names the input that the prompt text fills.
  • Model settings are in env, the API key is under secrets, and your code passes them to crewai.LLM(...).
  • If the repository is private, GITHUB_TOKEN is declared under secrets.
  • If you use Action Gateway through the session endpoint, every tool is in tools and preload_tools with an allow rule, not ask, and your crew reads HARNESS_MCP_URL_DO_ACTIONS.
  • If you use Action Gateway through the PyDo client, DIGITALOCEAN_TOKEN is under secrets, api.digitalocean.com and actions.do-ai.run are in egress, and every tool your crew calls has an allow rule in session.create.
  • doctl harness-runtime validate passes, and no secrets are committed to the repository.

Troubleshoot CrewAI Sessions

Use this table to diagnose common CrewAI deployment errors:

Symptom Likely cause What to do
crewai.framework is required for adapter "crewai" No clone target Add crewai.framework.repo and crewai.framework.repo_sha.
crewai.framework.repo_sha "main" must be a full 40-character commit SHA A branch or tag name Use the exact commit SHA.
env.FRAMEWORK_REPO is deprecated warning Old env spelling The spec works as is. Move the values into crewai.framework when you can.
both set with different values; declare it once, under crewai.framework The same field in env.FRAMEWORK_* and crewai.framework Remove the env copy.
field "framework" is not a top-level flat field Spec written before framework and inputs moved under crewai: Nest both blocks under crewai:.
Session fails to boot; no crew running crewai.json missing at the resolved directory Add crewai.json, set crewai.framework.subdir, or set crewai.framework.object.
crewai.framework.repo rejected URL is not an HTTPS github.com address Use a full https://github.com/<owner>/<repo>.git URL.
Clone or fetch fails Private repository, bad SHA, or invalid token Check the URL and commit, and set a valid GITHUB_TOKEN.
Credential rejected under env API key or similar value placed under env Move it to secrets.
Size rejected Unsupported size value Use a current sandbox size slug: mars-1vcpu-1gb, mars-2vcpu-2gb, mars-2vcpu-4gb, mars-4vcpu-8gb, or mars-16vcpu-32gb.
run failed: Template variable 'topic' not found Nothing maps the prompt text onto the placeholder Set crewai.inputs.prompt_key, or a default for that input.
400 invalid inputs["..."] on a turn The turn breaks the declared crewai.inputs.schema Fix the named input: declared name, type, or a missing required value.
An integer input shows as 2.0 in task text Known limit for values sent in a turn Declare it as string, or rely on a default.
Crew calls only action_search, action_invoke, and action_code The tool is selected but not preloaded Add it to preload_tools.
permission rule tool ... must use its provider-qualified slug A bare tool name Use the provider-qualified slug, for example exa_web_search.
Crew has no Action Gateway tools and HARNESS_MCP_URL_DO_ACTIONS is unset The spec attaches no do.actions server Add - do.actions under tools.
Action Gateway session create failed, or no tools available (PyDo client) DIGITALOCEAN_TOKEN missing or invalid, or the sandbox cannot reach the DigitalOcean API Confirm the token is under secrets, and that api.digitalocean.com and actions.do-ai.run are in the sandbox egress allowlist.
Tool names are long and end in a hash Tools attached with Agent(mcps=[MCPServerHTTP(...)]) Use MCPServerAdapter or @CrewBase.
A tool with an ask rule runs without approval A crew has no approval surface Use allow or deny.
Preflight rejects the crew Task.human_input=True present Remove it. Asynchronous human-in-the-loop is not supported for this adapter.
Model call fails or hits the wrong endpoint HARNESS_INFERENCE_* not passed into LLM(...) Pass model=, base_url=, and api_key= explicitly.
ModuleNotFoundError at run time Dependency missing from your dependency file Add the package, push a new commit, and update crewai.framework.repo_sha.
Unexpected dependency install Both dependency files present Consolidate into one file. requirements.txt wins over pyproject.toml.
Session create fails on name Session name already in use Remove the old session or change name.
Run is terminal; create a new one The 10-minute idle timeout ended the run Start a new session and send input within about 10 minutes of each turn.

Next Steps

We can't find any results for your search.

Try using different keywords or simplifying your search terms.