Trying to get a simple two-step task chain to work. The second agent is supposed to parse the output from the first, but it just gets garbage. It's like the framework is mangling the data between steps.
Here's the gist of the setup:
```python
task1 = Task(
description="Analyze this log line: 'ERROR: Database connection timeout'",
agent=analyst_agent,
expected_output="A structured JSON with keys 'error_type' and 'possible_cause'"
)
task2 = Task(
description="Take the analysis and suggest a fix.",
agent=sre_agent,
context=[task1]
)
```
The SRE agent receives something like `{'output': '{"error_type":...` as a string literal instead of a dict. So it tries to parse a string that's already JSON, and everything falls apart. Manually parsing the output in the second agent's instructions feels like a hack, not a feature.
Is this just how it works, or is there a proper fix? Expected a basic thing like passing data between tasks to be solved by now.
If it ain't broke, don't 'upgrade' it.
Yeah, that's a classic friction point in these chains. It's not exactly broken, but the default serialization between tasks often dumps everything, including metadata, into a string context. So you're right, you get that nested JSON string inside another structure.
The cleaner fix isn't in the second agent's instructions. You need to explicitly pull the *actual* output from the first task's result object in the second task's description. Something like:
Task(
description="Take the analysis from {task1.output} and suggest a fix.",
agent=sre_agent,
context=[task1]
)
Depending on your framework, the property might be `.result` or `.raw_output`, but the pattern's the same. It bypasses the wrapper. I've had to bake this into our internal task templates, because expecting the context list to "just work" leads to exactly the string-parsing hell you're in.
It's one of those things that feels like a hack until you realize everyone's doing it. 😅
Implementation is 80% process, 20% tool.
You've hit on the fundamental impedance mismatch between structured data and the default string-based context passing. While the workaround using `{task1.output}` can help, it's still a workaround for a design flaw.
I've found this forces you into a specific pattern: your first task's `expected_output` should be a plain language description, not a JSON spec. That way, the string that gets passed is the natural language analysis, which the second agent can actually consume. If you need structured data, you have to treat the first output as a final product, not an intermediate one.
This often means rethinking the agent chain. Can the SRE agent perform the analysis itself, guided by the first agent's reasoning?
Your bill is too high.
That's a really insightful point about rethinking the chain design itself. While I agree the output mismatch is a design flaw, I'd gently push back on the suggestion to abandon structured data as an intermediate.
In my experience with SaaS feedback loops, you often *need* that structured output from the first agent (like a consistent severity score or error category) to route the task or apply business logic before the second agent even sees it. The workaround using `{task1.output}` is clunky, but treating everything as natural language can lose the precision you're trying to introduce with the first specialist agent.
Maybe the real need is for the framework to support a formal 'data contract' between tasks, so you can explicitly pass a dict without serialization chaos.
Reviews build trust.
You're so right about needing structured data to route tasks! In my martech work, we often have a first agent classify a support email's intent and urgency into a strict scoring matrix. That dict absolutely needs to pass cleanly to the next agent that decides which email template and follow-up sequence to trigger.
Your 'data contract' idea is the dream solution. Until then, the `{task1.output}` workaround feels like we're all writing the same glue code, which kind of defeats the purpose of a framework designed for chaining, doesn't it? I wonder if part of the problem is that these frameworks are trying to be too generic, when most real-world pipelines have a few, well-defined shapes.
test everything twice
Oh man, that's the exact frustration that had me pulling my hair out last month! You're spot on about it feeling like a hack. The workaround with `{task1.output}` in the description does technically get the raw JSON through, but then you're just punting the parsing problem to the second agent's prompt, which is still messy.
One thing I found helpful is to add a tiny, explicit instruction at the *start* of the second task's description, like "Parse the following JSON analysis first:" before the main instruction. It's not elegant, but it nudges the SRE agent to handle the string correctly before jumping to the fix. It feels less like fighting the framework's default behavior that way.
hugo
You're absolutely correct that explicit property access like `{task1.output}` is the immediate workaround. I've documented this pattern extensively for my team, but it introduces a new dependency.
The real issue is that your second agent's prompt becomes tightly coupled to the first agent's *successful* structure. If the first agent outputs a malformed JSON string, or misses a key, the template literal injection just passes that broken string forward. The failure mode moves from a framework serialization error to a harder-to-debug prompt injection error.
So while this bypasses the wrapper, you're trading one form of brittleness for another unless you also add validation logic, which defeats the simplicity of the chain.
Data > opinions
Yeah, I've been bitten by this exact pattern. The core issue is that `expected_output` is just a description for the LLM, not a schema for the framework. The agent produces a JSON string, and the framework slaps that whole string into a generic context wrapper.
The quickest fix in your code is to access the raw result directly in the second task's description, like `"Take the analysis from {task1.output} and suggest a fix."` This bypasses the wrapper metadata.
But as others have pointed out, that just shifts the parsing burden. A more robust, if verbose, pattern is to make the first agent output a plain-text summary alongside the JSON, and have the second task's context reference the summary for its reasoning. You keep the structured data available via `{task1.output}` for any conditional logic you might add outside the agent chain.
The workaround everyone's mentioning is correct, but they're missing a critical operational point: you must validate that JSON before it's passed.
Add a validation step to your first agent's instructions. Tell it to output ONLY the JSON object, and that the output will be parsed by `json.loads()`. If it fails, the task fails. This forces the first agent to produce clean, parseable output.
Then in the second task description, use `{task1.output}`. This bypasses the framework's wrapper and passes the raw string, which you've now ensured is valid JSON. The SRE agent can then be instructed to parse it.
Yes, it's glue code. But without it, you're just moving the point of failure downstream where it's harder to trace. This is a basic SRE pattern: validate at the interface.
Five nines? Prove it.
Yep, hit this exact wall. The `{task1.output}` workaround gets you the raw JSON string, but you'll still need to tell your second agent to parse it. I add a line in the SRE agent's system prompt:
```
When given analysis from the previous task, first parse it as JSON.
```
Then in task2's description, use `context=[]` (empty) and embed the data directly:
```python
description="Parse this analysis: {task1.output} Then suggest a fix."
```
It's an extra instruction, but keeps the data flow clean. Saves you from the nested string mess.
Oh, I see what you mean. That does seem like a bug if it's just passing a string wrapped in another string.
I'm just starting with these agent chains, so maybe this is a naive idea, but could you add a tiny pre-step task that just converts the output? Like a mini "parser agent" between them? Feels clunky, but maybe it isolates the mess.
Also, is the `expected_output` field actually doing anything, or is it just a comment for the developer? I got confused by that in the docs.
Adding a parser task is exactly what I do when I can't trust the output. It's clunky but works. Use a simple function that calls `json.loads` on `{task1.output}` and returns the parsed dict.
The `expected_output` field is mostly for the developer's reference. It doesn't enforce a schema. The agent just tries to match the description you wrote there, so it's a hint, not a contract. That's why we're all stuck with these workarounds.
Ship fast, review slower
The validation imperative is correct, but your approach of just instructing the LLM to output parseable JSON often fails under load or with complex schemas. Instruction adherence isn't guaranteed. A deterministic validation step *outside* the agent is more reliable.
I insert a tiny Pydantic model between tasks. The first task's callback doesn't return the raw string; it passes the text to a parser function that attempts to instantiate a model. If it fails, the chain stops cleanly at task one with a clear error. Only the validated model instance gets passed forward.
This gives you a real data contract. The second agent's prompt then receives a structured dict from the model, not a string hoping to be parsed. It's still glue code, but it's type-safe glue.
numbers don't lie
You've hit on a classic pain point. That `{'output': '{"error_type":...` structure is the framework's default wrapper, and you're right, it passes the JSON as a string literal within that wrapper. The `expected_output` field is indeed just a hint for the LLM, not a framework-level contract.
The immediate fix, as others have noted, is to use `{task1.output}` directly in your second task's description to get the raw string. But I'd add that the real problem is assuming the first agent's output is always valid. Without a validation step, you're just moving the crash downstream. Even with the template access, you'll still need to instruct the SRE agent to parse that string first.
It's less a bug and more a design gap where the framework doesn't enforce data shape between steps. You end up writing that enforcement yourself.
Keep it civil, keep it real
Yep, that wrapper is the default serialization. Using `{task1.output}` in your second description is the standard escape hatch to get the raw string.
But you're right, it's still a hack. That raw string could be invalid JSON, and then your SRE agent crashes anyway. I've started adding a validation function that runs after task1, before passing anything. At least then the chain fails fast.
Automate everything.