---
title: Chaining
description: Learn how to combine multiple LLM calls in sequence to solve complex tasks through functional chaining, nested chains, conditional execution, and parallel processing.
---

# Chaining

<Note>
  If you haven't already, we recommend first reading the section on [Calls](/docs/v1/learn/calls)
</Note>

Chaining in Mirascope allows you to combine multiple LLM calls or operations in a sequence to solve complex tasks. This approach is particularly useful for breaking down complex problems into smaller, manageable steps.

Before diving into Mirascope's implementation, let's understand what chaining means in the context of LLM applications:

1. **Problem Decomposition**: Breaking a complex task into smaller, manageable steps.
2. **Sequential Processing**: Executing these steps in a specific order, where the output of one step becomes the input for the next.
3. **Data Flow**: Passing information between steps to build up a final result.

## Basic Usage and Syntax

### Function Chaining

Mirascope is designed to be Pythonic. Since calls are defined as functions, chaining them together is as simple as chaining the function calls as you would normally:

<TabbedSection>
<Tab value="Shorthand">
```python
from mirascope import llm


@llm.call(provider="$PROVIDER", model="$MODEL")
def summarize(text: str) -> str: # [!code highlight]
    return f"Summarize this text: {text}"


@llm.call(provider="$PROVIDER", model="$MODEL")
def translate(text: str, language: str) -> str: # [!code highlight]
    return f"Translate this text to {language}: {text}"


summary = summarize("Long English text here...") # [!code highlight]
translation = translate(summary.content, "french") # [!code highlight]
print(translation.content)
```
</Tab>
<Tab value="Template">
```python
from mirascope import llm, prompt_template


@llm.call(provider="$PROVIDER", model="$MODEL")
@prompt_template("Summarize this text: {text}")
def summarize(text: str): ... # [!code highlight]


@llm.call(provider="$PROVIDER", model="$MODEL")
@prompt_template("Translate this text to {language}: {text}")
def translate(text: str, language: str): ... # [!code highlight]


summary = summarize("Long English text here...") # [!code highlight]
translation = translate(summary.content, "french") # [!code highlight]
print(translation.content)
```
</Tab>
</TabbedSection>

One benefit of this approach is that you can chain your calls together any which way since they are just functions. You can then always wrap these functional chains in a parent function that operates as the single call to the chain.

### Nested Chains

In some cases you'll want to prompt engineer an entire chain rather than just chaining together individual calls. You can do this simply by calling the subchain inside the function body of the parent:

<TabbedSection>
<Tab value="Shorthand">
```python
from mirascope import llm


@llm.call(provider="$PROVIDER", model="$MODEL")
def summarize(text: str) -> str: # [!code highlight]
    return f"Summarize this text: {text}"


@llm.call(provider="$PROVIDER", model="$MODEL")
def summarize_and_translate(text: str, language: str) -> str:
    summary = summarize(text) # [!code highlight]
    return f"Translate this text to {language}: {summary.content}" # [!code highlight]


response = summarize_and_translate("Long English text here...", "french")
print(response.content) # [!code highlight]
```
</Tab>
<Tab value="Template">
```python
from mirascope import BaseDynamicConfig, llm, prompt_template


@llm.call(provider="$PROVIDER", model="$MODEL")
@prompt_template("Summarize this text: {text}")
def summarize(text: str): ... # [!code highlight]


@llm.call(provider="$PROVIDER", model="$MODEL")
@prompt_template("Translate this text to {language}: {summary}") # [!code highlight]
def summarize_and_translate(text: str, language: str) -> BaseDynamicConfig:
    return {"computed_fields": {"summary": summarize(text)}} # [!code highlight]


response = summarize_and_translate("Long English text here...", "french")
print(response.content) # [!code highlight]
```
</Tab>
</TabbedSection>

We recommend using nested chains for better observability when using tracing tools or applications.

<Info title="Improved tracing through computed fields" collapsible={true} defaultOpen={false}>
  If you use computed fields in your nested chains, you can always access the computed field in the response. This provides improved tracing for your chains from a single call.

  <TabbedSection>
  <Tab value="Shorthand">
```python
from mirascope import BaseDynamicConfig, Messages, llm


@llm.call(provider="$PROVIDER", model="$MODEL")
def summarize(text: str) -> str:
    return f"Summarize this text: {text}"


@llm.call(provider="$PROVIDER", model="$MODEL")
def summarize_and_translate(text: str, language: str) -> BaseDynamicConfig:
    summary = summarize(text)
    return {
        "messages": [
            Messages.User(f"Translate this text to {language}: {summary.content}")
        ],
        "computed_fields": {"summary": summary},
    }


response = summarize_and_translate("Long English text here...", "french")
print(response.content)
print(
    response.model_dump()["computed_fields"]
)  # This will contain the `summarize` response
```
  </Tab>
  <Tab value="Template">
```python
from mirascope import BaseDynamicConfig, llm, prompt_template


@llm.call(provider="$PROVIDER", model="$MODEL")
@prompt_template("Summarize this text: {text}")
def summarize(text: str): ...


@llm.call(provider="$PROVIDER", model="$MODEL")
@prompt_template("Translate this text to {language}: {summary}")
def summarize_and_translate(text: str, language: str) -> BaseDynamicConfig:
    return {"computed_fields": {"summary": summarize(text)}}


response = summarize_and_translate("Long English text here...", "french")
print(response.content)
print(
    response.model_dump()["computed_fields"]
)  # This will contain the `summarize` response
```
  </Tab>
  </TabbedSection>
</Info>

## Advanced Chaining Techniques

There are many different ways to chain calls together, often resulting in breakdowns and flows that are specific to your task.

Here are a few examples:

<TabbedSection>
<Tab value="Conditional">
```python
from enum import Enum

from mirascope import BaseDynamicConfig, llm, prompt_template


class Sentiment(str, Enum):
    POSITIVE = "positive"
    NEGATIVE = "negative"


@llm.call(provider="$PROVIDER", model="$MODEL", response_model=Sentiment)
def sentiment_classifier(review: str) -> str:
    return f"Is the following review positive or negative? {review}"


@llm.call(provider="$PROVIDER", model="$MODEL")
@prompt_template(
    """
    SYSTEM:
    Your task is to respond to a review.
    The review has been identified as {sentiment}.
    Please write a {conditional_review_prompt}.

    USER: Write a response for the following review: {review}
    """
)
def review_responder(review: str) -> BaseDynamicConfig:
    sentiment = sentiment_classifier(review=review)
    conditional_review_prompt = (
        "thank you response for the review."
        if sentiment == Sentiment.POSITIVE
        else "response addressing the review."
    )
    return {
        "computed_fields": {
            "conditional_review_prompt": conditional_review_prompt,
            "sentiment": sentiment,
        }
    }


positive_review = "This tool is awesome because it's so flexible!"
response = review_responder(review=positive_review)
print(response)
print(response.dynamic_config)
```
</Tab>
<Tab value="Parallel">
```python
import asyncio

from mirascope import BaseDynamicConfig, llm, prompt_template
from pydantic import BaseModel


@llm.call(provider="$PROVIDER", model="$MODEL")
@prompt_template(
    """
    Please identify a chef who is well known for cooking with {ingredient}.
    Respond only with the chef's name.
    """
)
async def chef_selector(ingredient: str): ...


class IngredientsList(BaseModel):
    ingredients: list[str]


@llm.call(provider="$PROVIDER", model="$MODEL", response_model=IngredientsList)
@prompt_template(
    """
    Given a base ingredient {ingredient}, return a list of complementary ingredients.
    Make sure to exclude the original ingredient from the list.
    """
)
async def ingredients_identifier(ingredient: str): ...


@llm.call(provider="$PROVIDER", model="$MODEL")
@prompt_template(
    """
    SYSTEM:
    Your task is to recommend a recipe. Pretend that you are chef {chef}.

    USER:
    Recommend recipes that use the following ingredients:
    {ingredients}
    """
)
async def recipe_recommender(ingredient: str) -> BaseDynamicConfig:
    chef, ingredients = await asyncio.gather(
        chef_selector(ingredient), ingredients_identifier(ingredient)
    )
    return {"computed_fields": {"chef": chef, "ingredients": ingredients}}


async def run():
    response = await recipe_recommender(ingredient="apples")
    print(response.content)


asyncio.run(run())
```
</Tab>
<Tab value="Iterative">
```python
from mirascope import llm, prompt_template
from pydantic import BaseModel, Field


class SummaryFeedback(BaseModel):
    """Feedback on summary with a critique and review rewrite based on said critique."""

    critique: str = Field(..., description="The critique of the summary.")
    rewritten_summary: str = Field(
        ...,
        description="A rewritten summary that takes the critique into account.",
    )


@llm.call(provider="$PROVIDER", model="$MODEL")
def summarizer(original_text: str) -> str:
    return f"Summarize the following text into one sentence: {original_text}"


@llm.call(provider="$PROVIDER", model="$MODEL", response_model=SummaryFeedback)
@prompt_template(
    """
    Original Text: {original_text}
    Summary: {summary}

    Critique the summary of the original text.
    Then rewrite the summary based on the critique. It must be one sentence.
    """
)
def resummarizer(original_text: str, summary: str): ...


def rewrite_iteratively(original_text: str, summary: str, depth=2):
    text = original_text
    for _ in range(depth):
        text = resummarizer(original_text=text, summary=summary).rewritten_summary
    return text


original_text = """
In the heart of a dense forest, a boy named Timmy pitched his first tent, fumbling with the poles and pegs.
His grandfather, a seasoned camper, guided him patiently, their bond strengthening with each knot tied.
As night fell, they sat by a crackling fire, roasting marshmallows and sharing tales of old adventures.
Timmy marveled at the star-studded sky, feeling a sense of wonder he'd never known.
By morning, the forest had transformed him, instilling a love for the wild that would last a lifetime.
"""

summary = summarizer(original_text=original_text).content
print(f"Summary: {summary}")
rewritten_summary = rewrite_iteratively(original_text, summary)
print(f"Rewritten Summary: {rewritten_summary}")
```
</Tab>
</TabbedSection>

[Response Models](/docs/v1/learn/response_models) are a great way to add more structure to your chains, and [parallel async calls](/docs/v1/learn/async#parallel-async-calls) can be particularly powerful for making your chains more efficient.

## Next Steps

By mastering Mirascope's chaining techniques, you can create sophisticated LLM-powered applications that tackle complex, multi-step problems with greater accuracy, control, and observability.

Next, we recommend taking a look at the [Response Models](/docs/v1/learn/response_models) documentation, which shows you how to generate structured outputs.