---
title: Task Results
description: Validate task outputs with structured result types.
icon: square-check
---

ControlFlow tasks are designed to translate between the unstructured, conversational world of your AI agents and the structured, programmatic world of your application. The primary mechanism for this translation is the task's result, which should be a well-defined, validated output that can be used by other tasks or components in your workflow.

## Structured results

ControlFlow allows you to specify the expected structure of a task's result using the `result_type` parameter. This ensures that the result conforms to a specific data schema, making it easier to work with and reducing the risk of errors in downstream tasks.

### Strings

By default, the `result_type` of a task is a string, which essentially means the agent can return any value that satisfies the task's objective.

For example, if you ask an agent to "Say hello in three languages", it might return a simple string like `"Hello; Hola; Bonjour"` or a more complex, conversational response instead:

<CodeGroup>
```python Code
import controlflow as cf

result = cf.run("Say hello in three languages")

print(result)
```

```text Simple result
Hello; Hola; Bonjour
```

```text Complex result
Hello there!

In three languages, "Hello" can be expressed as follows:

1. English: Hello
2. Spanish: Hola
3. French: Bonjour
```
</CodeGroup>

Sometimes this flexibility is useful, especially if your task's result will only be consumed as the input to another ControlFlow task. However, it can also lead to ambiguity and errors if the agent produces unexpected output, and is difficult to work with in an automated or programmatic way.


### Numbers

If your result is a number, you can specify the `result_type` as `int` or `float`:

<CodeGroup>
```python Code
import controlflow as cf

result = cf.run("What is 2 + 2?", result_type=int)

print(result)
assert isinstance(result, int)
```
```text Result
4
```
</CodeGroup>

### Booleans
You can use `bool` for tasks whose result is a simple true/false value:

<CodeGroup>
```python Code
import controlflow as cf

result = cf.run("The Earth is flat", result_type=bool)

print(result)
assert result is False
```
```text Result
False
```
</CodeGroup>


### Collections

You can also use typed collections like lists and dicts to specify the structure of your task's result.

Let's revisit the example of asking an agent to say hello in three languages, but this time specifying that the result should be a list of strings, or `list[str]`. This forces the agent to produce the result you probably expected (three separate strings, each representing a greeting in a different language):

<CodeGroup>
```python Code
import controlflow as cf

result = cf.run("Say hello in three languages", result_type=list[str])

print(result)
print(result[0])
```

```text Result
['Hello', 'Hola', 'Bonjour']
'Hello'
```
</CodeGroup>

### Annotated types

Sometimes, data types are not precise enough to guide the agent to the desired result. In these cases, you can use an annotated type to provide more specific instructions.

For example, if we want to ensure that the agent returns a string that is only a zip code, we can specify the `result_type` as `Annotated[str, "a 5 digit zip code"]`.

<CodeGroup>
```python Code
import controlflow as cf

result = cf.run(
    "What is the zip code of the White House?",
    result_type=Annotated[str, "a 5 digit zip code"],
)

print(result)
```

```text Result
20500
```
</CodeGroup>

<Tip>
Note that annotated types are not validated; the annotation is provided as part of the agent's natural language instructions. You could additionaly provide a custom [result validator](#validation) to enforce the constraint.
</Tip>

### Labeling / classification

Often, you may want an agent to choose a value from a specific set of options, in order to label or classify a response as one of potentially many choices.


To do this, specify a list, tuple, `Literal`, or enum of allowed values for the result type. Here, we classify the media type of "Star Wars: Return of the Jedi" from a list of options:

<CodeGroup>
```python Code
import controlflow as cf

media = cf.run(
    "Star Wars: Return of the Jedi",
    result_type=["movie", "tv show", "book", "comic", "other"]
)

print(media)
```

```text Result
movie
```
</CodeGroup>

<Tip>
ControlFlow optimizes single-choice constrained selection by asking agents to choose a value by index rather than writing out the entire response. This optimization significantly improves latency while also conserving output tokens.
</Tip>

You can provide almost any Python object as a constrained choice, and ControlFlow will return *that object* as the result. Note that objects must be serialized in order to be shown to the agent.

#### A list of labels


When you provide a set of constrained choices, the agent will choose **one and only one** as the task's result. Often, you will want to produce a list of labels, either because you want to classify multiple items at once OR because you want to allow the agent to choose multiple values for a single input. To do so, you must indicate that your expected result type is a list of either `Literal` values or enum members.

In the following example, two media types are provided as context, and because the result type is a list, the agent is able to produce two responses:

<CodeGroup>
```python Code
import controlflow as cf
from typing import Literal

media = cf.run(
    ["Star Wars: Return of the Jedi", "LOST"],
    result_type=list[Literal["movie", "tv show", "book", "comic", "other"]]
)

print(media)
```

```text Result
['movie', 'tv show']
```
</CodeGroup>

In this example, the agent is able to choose multiple values for a single input, and the result is a list of strings:

<CodeGroup>
```python Code
import controlflow as cf
from typing import Literal

tags = cf.run(
    'Star Wars: Return of the Jedi',
    instructions="Choose all that apply",
    result_type=list[Literal["Star Wars", "space", "pirates", "adventure", "musical"]]
)

print(tags)
```

```text Result
['Star Wars', 'space', 'adventure']
```
</CodeGroup>

<Warning>
Labeling multiple inputs at once relies on Python's built-in type annotations and does not provide the same list- and tuple-aware optimizations and sugar that ControlFlow provides for single-choice constrained selection. Therefore the following syntax, which is not considered proper Python, will error:

```python
cf.run(
    ...,
    result_type=list[["A", "B"]]
)
```

but using a `Literal` or enum will work:

```python
cf.run(
    ...,
    result_type=list[Literal["A", "B"]]
)
```
</Warning>

### Pydantic models

For complex, structured results, you can use a Pydantic model as the `result_type`. Pydantic models provide a powerful way to define data schemas and validate input data.

<CodeGroup>
```python Code
import controlflow as cf
from pydantic import BaseModel, Field

class ResearchReport(BaseModel):
    title: str
    summary: str
    key_findings: list[str] = Field(min_items=3, max_items=10)
    references: list[str]

result = cf.run(
    "Generate a research report on quantum computing",
    result_type=ResearchReport,
)

print(repr(result))
```

```text Result
ResearchReport(
    title='Quantum Computing: Current Landscape and Future Prospects',
    summary='Quantum computing represents a significant leap in computational capabilities, leveraging the principles of quantum mechanics to perform complex calculations far beyond the reach of classical computers. This report delves into the current state of quantum computing, exploring its foundational principles, recent advancements, and the potential implications for various industries. Key findings highlight the technological hurdles, notable achievements, and the transformative potential of quantum computing in solving intractable problems.',
    key_findings=[
        'Principles of Quantum Mechanics: Quantum computing utilizes qubits, superposition, and entanglement to process information in fundamentally new ways, enabling parallel computation on a massive scale.',
        'Technological Achievements: Major milestones include the development of stable qubits, error correction algorithms, and quantum supremacy demonstrations by leading tech companies like Google and IBM.',
        'Applications and Impacts: Quantum computing shows promise in fields such as cryptography, materials science, pharmaceuticals, and artificial intelligence, potentially revolutionizing these sectors by providing unprecedented computational power.',
        'Challenges and Limitations: Significant obstacles remain, including qubit stability, error rates, and the need for extremely low temperatures. Overcoming these challenges is essential for the practical deployment of quantum computers.',
        'Future Directions: Ongoing research focuses on improving qubit coherence times, developing scalable quantum architectures, and creating robust quantum algorithms to harness the full potential of quantum computing.'
    ],
    references=[
        'Nielsen, M. A., & Chuang, I. L. (2010). Quantum Computation and Quantum Information. Cambridge University Press.',
        'Arute, F., Arya, K., Babbush, R., Bacon, D., Bardin, J. C., Barends, R., ... & Martinis, J. M. (2019). Quantum supremacy using a programmable superconducting processor. Nature, 574(7779), 505-510.',
        'Preskill, J. (2018). Quantum Computing in the NISQ era and beyond. Quantum, 2, 79.',
        'Montanaro, A. (2016). Quantum algorithms: an overview. npj Quantum Information, 2, 15023.',
        'Shor, P. W. (1997). Polynomial-Time Algorithms for Prime Factorization and Discrete Logarithms on a Quantum Computer. SIAM Journal on Computing, 26(5), 1484-1509.'
    ]
)
```
</CodeGroup>

### No result

Sometimes, you may want to ask an agent to perform an action without expecting or requiring a result. In this case, you can specify `result_type=None`. For example, you might want to ask an agent to use a tool or post a message to the workflow thread, without requiring any task output.

```python
import controlflow as cf

def status_tool(status: str) -> None:
    """Submit a status update to the workflow thread."""
    print(f"Submitting status update: {status}")

cf.run(
    "Use your tool to submit a status update",
    result_type=None,
    tools=[status_tool],
)
```

Note that it is generally recommended to ask agents to produce a result, even if its just a quick status update. This is because other agents in the workflow can usually see the result of a task, but they may not be able to see any tool calls, messages, or side effects that the agent used to produce the result. Therefore, results can be helpful even if the assigned agent doesn't need them.


## Validation

### Pydantic

When using a Pydantic model as the `result_type`, you can use any of Pydantic's built-in or custom validators to further constrain or modify the result after it has been produced.

<CodeGroup>
```python Code
import controlflow as cf
from pydantic import BaseModel, field_validator

class SentimentAnalysis(BaseModel):
    text: str
    sentiment: float

    @field_validator('sentiment')
    def check_sentiment_range(cls, v):
        if not -1 <= v <= 1:
            raise ValueError('Sentiment must be between -1 and 1')
        return v

result = cf.run(
    "Analyze sentiment of given text",
    result_type=SentimentAnalysis,
    context=dict(text="I love ControlFlow!"),
)

print(repr(result))
```

```text Result
SentimentAnalysis(text='I love ControlFlow!', sentiment=0.9)
```
</CodeGroup>

### Validation functions

If you supply a function as your task's `result_validator`, it can be used to further validate or even modify the result after it has been produced by an agent.

The result validator will be called with the LLM result **after** it has been coerced into the `result_type`, and must either return a validated result or raise an exception. ControlFlow supplies a few common validators to get you started:

- `between(min_value, max_value)`: Validates that the result is a float between `min_value` and `max_value`.
- `has_len(min_length, max_length)`: Validates that the result is a string, list, or tuple with a length between `min_length` and `max_length`.
- `has_keys(required_keys)`: Validates that the result is a dictionary with all of the specified keys.
- `is_url()`: Validates that the result is a string that is a URL.
- `is_email()`: Validates that the result is a string that is an email address.

These are available in the `controlflow.tasks.validators` module, along with a convenient `chain` function that allows you to combine multiple validators into a single function.


<Tip>
Remember that result validators must either **return** the result or **raise** an exception. They are not true/false checks!
</Tip>

```python
import controlflow as cf
from controlflow.tasks.validators import chain, between

def is_even(value: int) -> int:
    if value % 2 != 0:
        raise ValueError("Value must be even")
    return value

cf.run(
    "Generate an even number between 1 and 100",
    result_type=int,
    result_validator=chain(between(1, 100), is_even),
)
```

### Modifying the result

You can also use a result validator to modify the result after it has been produced by an agent. For example, you might want to round a floating point number or convert a string to a specific format.

```python
import controlflow as cf

def round_to_one_decimal_place(value: float) -> float:
    return round(value, 1)

sentiment = cf.run(
    "Analyze sentiment of given text",
    result_type=float,
    context=dict(text="I love ControlFlow!"),
    result_validator=round_to_one_decimal_place,
)
```
