---
title: CrewAI
description: Connect a CrewAI crew to Redline — experiments, production monitoring and runtime protection. The complete document, written to be followed by a coding agent inside the repository.
---

You are connecting a repository whose agent is a **CrewAI** crew to the Redline
platform: experiments AND production monitoring. Follow these steps exactly
and do not modify the repository's existing crew — the whole integration is
ONE new file plus TWO instrumentation packages.

## 0. Confirm the shape

Grep for `crewai`, `Crew(`, `Agent(` from `crewai`. If present, this is the
page. CrewAI is **Shape C**: it is NOT patched by the SDK, so the doorway must
merge the experiment's attached tools itself (§4). Its monitoring is still one
`pip install` away, because Arize's OpenInference instrumentation covers it.

## 1. Install and scaffold

At the repository root, inside its virtualenv:

```bash
pip install redlineai-sdk \
  openinference-instrumentation-crewai openinference-instrumentation-openai
redline init
```

**Both** instrumentation packages. CrewAI 1.x calls the model through the
OpenAI client, so the CrewAI package alone reports the crew's steps and none
of the model calls under them. Install now, before the first `redline dev`.
`redline init` writes `agents.py` — never `redline.py`.

## 2. Find the agent's entry point

Locate where the crew is kicked off: `crew.kickoff(`, `crew.kickoff_async(`,
or the repository's own `run(...)` that wraps it. Read what it takes — a
string, or an `inputs` dict — and what it returns. **Do NOT modify it.**

## 3. Wire it into `agents.py`

```python title="agents.py"
import asyncio, json
from redline import agent, redline_tools
import my_app.support_crew as subject                    # unchanged

async def _run(task, ctx) -> str:
    attached = await redline_tools(task)                 # §4 Shape C
    prompt = task.prompt

    if attached.list:
        # 5a. Named from the list, so it describes whatever was attached.
        brief = "## Tools this experiment attached\n" + "\n".join(
            f"- {t.name}: {t.description}" for t in attached.list)
        prompt = f"{prompt}\n\n{brief}\n"
        ctx.log(f"attached {len(attached.list)} tools: {', '.join(attached.names())}")

    advertised = attached.as_openai_schema()             # what to advertise
    if advertised:
        prompt += ("\nTo use one of the attached tools, reply with exactly one line:\n"
                   'REDLINE_TOOL {"name": "<tool>", "arguments": {…}}\n'
                   "You will be given its output and may then continue.\n"
                   f"Their schemas: {json.dumps(advertised)}\n")

    reply = await asyncio.to_thread(subject.run, prompt)

    # The loop: while the crew asks for an attached tool, run it and hand back
    # the observation. 5b — call() returns the error text rather than raising.
    for _ in range(12):
        line = next((l for l in reply.splitlines() if l.strip().startswith("REDLINE_TOOL")), None)
        if not line:
            break
        try:
            request = json.loads(line.split("REDLINE_TOOL", 1)[1].strip())
            name, arguments = request.get("name", ""), request.get("arguments", {})
        except Exception as exc:
            ctx.log(f"unparseable tool request: {exc}"); break
        ctx.tool(name, arguments)
        output = await attached.call(name, arguments)    # what to run when picked
        ctx.tool_result(name, output)
        reply = await asyncio.to_thread(
            subject.run, f"{prompt}\n\nYou asked for {name}. Its output:\n{output}\n\nContinue.")
    return reply

@agent(id="store-support", name="Store Support Crew",
       description="A crew running the store's support policy: order lookup, policy lookup, escalation.")
def run(task, ctx):
    return asyncio.run(_run(task, ctx))
```

- `id` is a kebab-case slug. `task.prompt` is the instructions. RETURN the
  final answer as a string.
- `asyncio.run()` is acceptable **only** because the doorway is a plain `def`
  and CrewAI holds no async resources between calls. A framework that does
  (Agno's pools) must use `async def run` and await instead.
- If `kickoff` takes an `inputs` dict, put the prompt in the field the crew's
  task template reads.

## 4. Attached tools — you MUST merge them yourself

An experiment can attach MCP servers and, on every run, the project's Linux
machine as a `machine_run` shell tool. A crew has no constructor the SDK can
wrap, so:

```python
attached = await redline_tools(task)
attached.as_openai_schema()                       # what to advertise
await attached.call(name, arguments)              # what to run when picked
```

`as_openai_schema()` is the plain function-calling shape and `call()` executes
by name — neither knows anything about your framework. **Do not hand-write an
adapter over `attached.list`**; these two are the supported path. The `REDLINE_TOOL`
line protocol in §3 is how the crew asks for one, since its own tool list is
fixed at construction. Attached SKILLS need nothing — they arrive inside
`task.prompt`.

## 5. Three failures that are nearly guaranteed if you skip them

**5a. Tell the agent its tools exist.** The brief in §3, GENERATED FROM THE
LIST so it names whatever was attached rather than a tool you hardcoded.
Without it the crew answers "I have no way to do that".

**5b. A tool that raises must not kill the run.** `attached.call()` returns a
failing tool's error **as text**, so the loop in §3 is already safe. Wrap the
crew's own tools the same way.

**5c. Do not drop the reasoning.** `ctx.tool` / `ctx.tool_result` around each
attached call, as in §3; `ctx.log` for counts and decisions. The crew's own
steps arrive via the instrumentation (§8a).

## 6. Connect and verify

The crew's environment must load the way the repo normally loads it. Then, in
`.env` at the repo root or exported:

```bash
REDLINE_URL=https://tryredlineai.co
REDLINE_API_KEY=rl_…          # Agents page → Runner key
redline dev
```

`redline dev` reads `.env` itself — every variable in it.

`redline dev` **must be running on this machine at all times**: experiments
execute inside it, the agent shows ONLINE only while it runs, snapshots for
Protect upload through it, policies and honeypots re-arm through it every 20
seconds, dev sessions stream to Monitor through it.

- `REDLINE_URL` — the Redline platform's address; nothing here hosts it.
- `REDLINE_API_KEY` — the runner key from the Agents page. **Do not commit it.**

On a server or VM: `nohup redline dev > ~/redline-dev.log 2>&1 &` then
`disown`. Outbound only. Success:
`connected — store-support registered and online`.

## 7. When something is wrong

- **"no agents found"** → `agents.py` exports no `@agent` function.
- **Registration rejected** → id collides; pick another slug.
- **The crew says it has no tools / never uses `machine_run`** → §4 not done,
  or the §5a brief missing.
- **The run dies on the first tool error** → §5b.
- **Session shows only User/Agent rows, "0 tool calls"** → one or both §8a
  packages missing, or installed after `redline dev` started. Install both
  and restart. A healthy session has model calls under every crew step.
- **`ask_user` ENDS the run** — return the questions as the answer.

Everything else: [known issues](/integrations/known-issues).

## 8. Monitoring — the same agent, watched in production

Nothing extra to write for dev: while `redline dev` is up, every conversation
ALSO streams to **Monitor → Sessions**.

**8a. REQUIRED — both packages, before `redline dev` starts:**
`openinference-instrumentation-crewai` **and**
`openinference-instrumentation-openai`. The SDK only LISTENS; CrewAI emits
nothing without them. A session of bare turns — no model calls, no token
count, "0 tool calls" — is this, every time.

**For the DEPLOYED agent** — real users, no `redline dev`:

```python title="agents.py"
from redline import observe

@observe(agent="store-support", session_arg="conversation_id",
         user_arg="user_id", input_arg="text")
async def handle_message(conversation_id: str, user_id: str, text: str) -> str:
    return await asyncio.to_thread(subject.run, text)
```

Config is environment only; without `REDLINE_API_KEY` the decorator is inert.

**8b. Your application has to CALL `handle_message`.** The repository's
existing message pipeline must call it — one call-site change. Without it,
production sessions never appear.

**8c. It must return, not yield.** `@observe` does not wrap async generators.

What the platform does with a session: Monitor → Sessions with the waterfall;
violations per span and re-judged within minutes; intents mined on close. See
[monitoring](/integrations/monitoring).

## 9. Runtime protection — what to expect once connected (nothing to write)

CrewAI's model calls go through the OpenAI client (via litellm), which the
SDK patches. Policies, the guard classifier and honeypots configured in the
console reach the running process within ~20 seconds. A denied tool call is
stripped from the model's response before the crew executes it and replaced
with `[redline] The call to <tool> was denied by policy: <reason>`; the guard
scores every user message and tool result; honeypots are injected beside the
crew's own tools. Streaming passes through unjudged; everything fails OPEN.
