---
audience: engineers
summary: Extend the quickstart's greet flow with a @flexible block that calls an LLM
  with a structured output.
title: Add an LLM call
path: tutorials/add-an-llm-call
status: published
---

The quickstart's `greet` flow returned a hard-coded string. Real flows use LLMs. This tutorial adds a `@flexible` block to the greet flow: it calls a model with a goal and an input, and gets back a structured output the runtime type-checks. It assumes you've completed the [quickstart](../quickstart) and have `greet.scaicore` on disk.

## 1. Declare a model role

LLM calls go through *roles*, not model names. A role is a slot the runtime can swap providers behind. Declare one in the `@core` block:

```scaicore
@core HelloWorld {
    version = "1.0.0"
    instance = :stateless

    @llm {
        primary = {
            model = "scailabs/poolnoodle-omni"
            temperature = 0.4
            modalities = [:text, :structured_output]
        }
    }
}
```

`primary` is conventional for the main role; you can declare additional roles (`reviewer`, `extractor`, …) and reference them by name from `@flexible` blocks via the `llm_role` field.

## 2. Declare an output type

Structured outputs need a type the compiler can verify against. Declare it at module scope:

```scaicore
type Greeting = {
    message: string,
    mood: Mood
}

type Mood = enum[happy, neutral, encouraging]
```

The runtime translates `Greeting` into a JSON schema and asks the provider to honor it.

## 3. Replace the @rigid body with @flexible

The old `greet` flow:

```scaicore
@flow greet(name: string): string {
    @rigid {
        return "Hello, ${name}!"
    }
}
```

Becomes:

```scaicore
@flow greet(name: string): Greeting {
    result = @flexible {
        goal = "Generate a warm, contextual greeting for the named person"
        input = { name: name }
        output: Greeting
    }
    @rigid {
        return result
    }
}
```

Three things changed: the flow's return type is now `Greeting`, the work moved into a `@flexible` block that asks the model for a `Greeting`-shaped value, and a small `@rigid` returns it. The `result =` binding captures the `@flexible` block's output into the surrounding scope.

## 4. Check it

`scaicore check` runs the full pipeline including type-check and verifier. It should pass:

```bash
scaicore check greet.scaicore
```

If you see `E206 — undefined model` you misspelled a role name. If you see `E300 — type mismatch` your `output:` doesn't line up with the flow's declared return type — they need to match.

## 5. Run it

`scaicore run` will compile and execute, but the CLI host's default model provider is a stub that returns mock values. To run against a real provider you'll either:

- Embed the runtime in your own host and pass a real `ModelProvider` implementation, or
- Configure the CLI host with provider credentials (see your model provider's docs for the role config).

Either way:

```bash
scaicore run greet.scaicore --flow greet --input '{"name": "Ada"}'
```

A successful run prints the structured output, e.g.:

```json
{
  "message": "Welcome back, Ada — good to see you again.",
  "mood": "encouraging"
}
```

## What you gained

Compared to the quickstart flow you now have: structured output the runtime type-checks against `Greeting`, a role-based model declaration that lets you swap providers without touching flow source, and (once a real provider is wired up) text generation that adapts to the input.

Next: [Add a human approval step](./human-approval) to require sign-off before the greeting is returned.
