# Share a CrewAI Crew's Output — Beyond output_file

Canonical: https://commareports.com/agents/share-crewai-output
Published: 2026-09-14

> CrewAI's output_file writes report.html to the machine that ran the crew. Publish the final task's result instead and the crew kickoff returns a URL anyone can open.

# Share what a CrewAI crew produced

A research crew runs for six minutes. The writer agent hands off to the editor
agent, the editor produces the final markdown, and `output_file="report.html"`
drops it next to your script.

Then someone asks for it, and the honest answer is "it's on my machine."

## output_file is an artifact, not a channel

`output_file` is doing exactly what it says. The problem is that a crew is
usually run somewhere other than where the reader sits — a scheduled container,
a Docker image, CrewAI's own deployment surface — and none of those hand out
filesystem access.

Two ways to fix it, depending on how much you trust the agents with the network.

## Option one — a publish tool on the last agent

```python
from crewai.tools import tool
import httpx, os

@tool("publish_report")
def publish_report(title: str, html: str) -> str:
    """Publish an HTML report and return its shareable URL."""
    r = httpx.post(
        "https://commareports.com/api/v1/reports",
        headers={"Authorization": f"Bearer {os.environ['COMMA_API_TOKEN']}"},
        json={"title": title, "html": html},
    )
    r.raise_for_status()
    return r.json()["url"]
```

Attach it to the editor, and make the expectation explicit in the task, because
a task that does not name its output shape will not produce one:

```python
Task(
    description="Assemble the findings into a single HTML report.",
    expected_output="The URL returned by publish_report, and nothing else.",
    agent=editor,
    tools=[publish_report],
)
```

## Option two — publish after kickoff

If you would rather the agents never touch HTTP, let the crew return the
document and publish it yourself:

```python
result = crew.kickoff(inputs={"topic": topic})
url = publish(title=f"{topic} — research", html=render(result.raw))
print(url)
```

This is the version to prefer for anything scheduled. The crew stays
deterministic in what it produces, and the publish step is ordinary code you
can retry, rather than a tool call an agent might decide to skip.

## Feedback as the next input

The interesting part is the return leg. A crew that can list the comment
threads on its last report gets a brief that is grounded in what actually
bothered the reader:

> Fetch the open comments on report `<id>`. Treat each one as a revision
> request, produce an updated report, update the same report id, and reply in
> each thread saying what changed.

That is the loop described in
[letting an agent respond to comments](/agents/let-an-agent-respond-to-comments).
Updating the same id matters — a second link means the reviewer's comments are
stranded on a page nobody is looking at any more.

## Worth knowing

- **Hierarchical crews should publish from the manager.** The worker agents
  hold fragments; the manager holds the document.
- **`reports:write` is the whole scope a crew needs.** Add `comments:read`
  only for the revision loop — see
  [scoped tokens for AI agents](/agents/scoped-tokens-for-ai-agents).
- **5 MB per report body.**
- **Keep the URL, drop the HTML.** Passing a rendered document between tasks
  spends context on every subsequent step.

## Try it

Free — unlimited reports, commenters and revisions.

**[Read the API reference →](/docs/api)**

### Related

- [Where should my agent post?](/agents/where-should-my-agent-post) ·
  [Context budgeting for AI reports](/agents/context-budgeting-ai-reports)
- [Share a LangGraph run's output](/agents/share-langgraph-output) ·
  [Share an AutoGen run's output](/agents/share-autogen-output)
- [The API](/docs/api) · [Routines](/docs/routines)
