Skip to content

LLMStep

LLMStep is the general LLM step. For each record it fills a prompt template, sends it to a served model, and writes the answer back as one or more new columns.

The specialized LLM steps write the prompt for you. LLMStep is the one where you write it yourself.

At a glance

Parameter Type Default Meaning
prompt str \| Path \| list \| Sample required prompt template, or a path to a file holding one
input_columns list[str] required columns injected into the template
model ServedModel \| list \| Sample required the served model(s) to call
output_column str "generated" column the answer is written to (text mode)
output_columns list[str] \| None None columns the answer is split into (json/xml mode)
parse_mode str "text" "text", "json" or "xml"
num_outputs int 1 answers per prompt × model × language
language str \| list \| dict \| Sample \| None None language(s) to generate in
forward_columns list[str] \| None None keep only these input columns
exclude_columns list[str] \| None None drop these input columns
skip_if Callable[[Record], bool] \| None None return True to skip a record
system_prompt str \| None None system message put before the prompt
on_parse_error str "skip" "skip" or "raise" when parsing fails

Everything after model is keyword-only.

The prompt

The prompt template uses {column} placeholders. Each column you name in it must be listed in input_columns, and each column in input_columns is looked up on the record and injected.

from datafast import LLMStep, openai

step = LLMStep(
    prompt="Write one short question about {topic}",
    input_columns=["topic"],
    output_column="question",
    model=openai(),
)

A placeholder that is not in input_columns raises KeyError when the step runs. A literal brace in the prompt must be doubled — {{ and }} — or it is read as a placeholder.

prompt also accepts a file path, as a str or a Path. The file is read on first use and cached.

from pathlib import Path

from datafast import LLMStep, openai

step = LLMStep(
    prompt=Path("prompts/summarize.txt"),
    input_columns=["text"],
    output_column="summary",
    model=openai(),
)

A missing file raises FileNotFoundError at the first record, before any call is made. That covers every Path, and any string that reads as a path to a prompt file — no whitespace, and one of the suffixes .txt, .md, .jinja, .jinja2, .j2, .prompt, .tmpl. Any other string is the prompt itself.

The output

parse_mode decides how one raw answer becomes columns.

parse_mode What it does Columns to declare
"text" writes the whole answer, stripped, to one column output_column
"json" reads the answer as a JSON object and takes one column per key output_columns
"xml" reads <name>...</name> tags and takes one column per tag output_columns

json and xml require output_columns; without it the constructor raises ValueError. Both modes also append format instructions to the end of your prompt, naming the columns they expect, so you do not have to describe the format yourself.

from datafast import LLMStep, openai

step = LLMStep(
    prompt="Write a question and its answer about {topic}",
    input_columns=["topic"],
    output_columns=["question", "answer"],
    parse_mode="json",
    model=openai(),
)

In json mode a missing key gives an empty string in that column, with a warning. In xml mode a missing tag does the same; tag matching ignores case. Answers wrapped in markdown code fences are unwrapped before the JSON is read.

output_column and output_columns are not alternatives to each other: if you pass output_columns in text mode, the answer goes into its first entry and the rest are never written, while output_column is ignored.

How many LLM calls

A step fans out. For every input record it makes one call per combination of:

prompts × served models × languages × num_outputs

Each call produces one output record, so the step does not preserve record counts — it multiplies them.

Records in Prompts Served models Languages num_outputs Calls and records out
100 1 1 — 1 100
100 2 1 — 1 200
100 2 2 — 1 400
100 2 2 3 2 2400

No language counts as one, not zero. Multiply before you run: this is the number you pay for.

from datafast import LLMStep, openai

step = LLMStep(
    prompt=["Summarize: {text}", "List the key points of: {text}"],
    input_columns=["text"],
    output_column="result",
    model=[openai(), openai("gpt-6.1-sol")],
    num_outputs=2,
)

Eight calls per record: 2 prompts × 2 served models × 1 language × 2 outputs.

Languages

language adds a language axis to that fan-out and gives the template two extra placeholders: {language} for the code and {language_name} for the name.

Value Meaning
None no language axis, no placeholders
"fr" one language; code and name are both "fr"
["en", "fr"] two languages; code and name are the same for each
{"en": "English", "fr": "French"} codes as keys, names as values
from datafast import LLMStep, openai

step = LLMStep(
    prompt="Write one sentence about {topic} in {language_name}",
    input_columns=["topic"],
    output_column="sentence",
    model=openai(),
    language={"en": "English", "fr": "French"},
)

Use the dict form when the prompt should name the language, since a model reads French better than fr.

Which columns come through

By default every column of the input record is copied to the output record, then the generated columns are added on top.

Parameter Type Default Meaning
forward_columns list[str] \| None None keep only these input columns
exclude_columns list[str] \| None None keep everything except these
from datafast import LLMStep, openai

step = LLMStep(
    prompt="Summarize: {text}",
    input_columns=["text"],
    output_column="summary",
    model=openai(),
    forward_columns=["doc_id"],
)

forward_columns wins if both are given; exclude_columns is then ignored. A generated column with the same name as an input column overwrites it.

Skipping records and system prompts

Parameter Type Default Meaning
skip_if Callable[[Record], bool] \| None None called per record; True means no call
system_prompt str \| None None sent as a system message before the prompt
from datafast import LLMStep, openai

step = LLMStep(
    prompt="Summarize: {text}",
    input_columns=["text"],
    output_column="summary",
    model=openai(),
    skip_if=lambda record: len(record["text"]) < 100,
    system_prompt="You are a concise technical writer.",
)

A skipped record is dropped, not passed through. If you want to keep it, split the pipeline with Filter and Concat instead.

Metadata columns

Every output record carries the step's own columns, all starting with _.

Column Written when Value
_model always the model id of the served model that answered
_prompt_index prompt is a list position of the prompt used, from 0
_language language is set the language code

With a single prompt and no language, the only added column is _model. All three names are reserved: compile() treats them as present downstream, so do not use them for your own data.

Temperature and max tokens

The step takes neither. It calls ServedModel.generate(), which takes no such arguments, so the served model's own settings decide temperature and length. Set them where the served model is built — see Served models.

Errors

on_parse_error is "skip" or "raise"; anything else raises ValueError at construction.

It applies the same way under Pipeline.run() as under the step's own process(), and it covers any failed call — a rate limit or a timeout as much as an unparseable answer. With "skip", compare the record count with the number you expected; with "raise", the run stops on the first failure.

Things worth knowing

  • Count the calls before you run. Records × prompts × served models × languages × num_outputs. Three small choices multiply into a large bill.
  • A mistyped prompt path raises. FileNotFoundError, before the first call.
  • json and xml add format instructions to the end of your prompt. Do not write your own on top of them.
  • A missing key or tag is an empty string, not a failure. The record is still yielded.
  • output_columns in text mode writes to its first entry only.
  • skip_if drops records rather than letting them pass.
  • _prompt_index is conditional. With one prompt it never appears; with a Sample of prompts it appears on some records and not others. Pass a list for a stable schema.

Where to go next