Skip to content

Your First Kedi Program

Your first program asks a model to complete a sentence, captures its answers as typed values, and uses those values in its output. No Python block or SDK call is needed.

Complete Installation, including OpenAI provider support and OPENAI_API_KEY, before running it. This program makes a real model request.

Write Your First Template

Create film.kedi:

> adapter: pydantic
> model: openai:gpt-5.6-luna

[film: str] = Spirited Away

>> The film <film> was directed by [director: str] and first released in [year: int].

= <film> (<year>), directed by <director>.
kedi film.kedi

In a uv project, use uv run kedi film.kedi. A typical result is:

Spirited Away (2001), directed by Hayao Miyazaki.

The program supplies the film title, not the answers. The model generates the director and release year; wording and factual correctness are not guaranteed. This example does not look up a film database.

  • [film: str] = ... binds the input title without calling a model.
  • >> opens a natural-language template. <film> substitutes the existing title.
  • [director: str] and [year: int] declare the answers to generate and their types. Both belong to the same template, not two separate prompts.
  • = ... returns the completed output, which the CLI prints. Reading the captured values resolves the template's deferred model request.

The completed template forms a sentence, while its captures become program values: director is a string and year is an integer. Kedi validates their types; that validation does not establish that the facts are true.

To choose the film at runtime, replace the input binding with:

[film: str] = `args.film`

Then run kedi film.kedi --film "My Neighbor Totoro". The template stays the same; only its input changes.

Grow the Program with a Typed Procedure

Once the template/capture distinction is familiar, a procedure can package a model interaction for reuse. This second example captures a structured review instead of individual film facts. Create review.kedi:

> adapter: pydantic
> model: openai:gpt-5.6-luna

~Review(decision: Literal["approve", "revise"], summary: str)

@review_change(title: str, diff_summary: str) -> Review:
  >> For change <title> with diff summary <diff_summary>, the review result is [review: Review].
  = `review`

= `review_change(args.title, args.diff_summary).model_dump_json()`

Run it:

kedi review.kedi \
  --title "Reject unsafe paths" \
  --diff-summary "Adds containment checks before file access"

Add Inputs

The output is a JSON object with decision and summary, for example:

{"decision":"revise","summary":"Show tests for traversal and symlink escape cases."}

This is illustrative, not an exact expected answer. The model sees only the title and summary, not the code or test results. Its recommendation is advisory: schema validation limits the decision to two values but does not prove that the change is safe or grant permission to merge it.

title and diff_summary are typed procedure parameters. The final call uses a single-backtick Python expression:

[review: Review] = `review_change(args.title, args.diff_summary)`

This passes native strings and preserves the native Review return. An angle call renders its result to text, so it is appropriate for procedures returning str, not for carrying a Review object through the dataflow.

Write a Template

Continuation rows after >> are joined with newlines and sent as one model request. This example needs only one row:

>> For change <title> with diff summary <diff_summary>, the review result is [review: Review].

<title> and <diff_summary> are substitutions. They read existing values; they do not ask the model to generate anything. Use substitutions for runtime facts, user input, prior procedure results, or deterministic Python values.

Capture a Typed Output

[review: Review] is an output capture. The selected adapter receives a schema derived from the Kedi type and must return a matching object. Capture output when downstream logic needs typed fields or when the response must be validated.

Kedi also exposes raw capture for deliberately unstructured provider text:

@review_change(title: str, diff_summary: str) -> str:
  [review] << Review of <title> with diff summary <diff_summary>:
  = <review>

Do not put output fields inside a raw << prompt. Raw captures always produce a string; types other than str are rejected. Prefer the typed version above for normal application dataflow, including typed str results.

Return the Result

Inside the typed version, = `review` returns the native Review model. The top-level expression serializes it deliberately:

= `review_change(args.title, args.diff_summary).model_dump_json()`

Use a native return when Python or another Kedi procedure needs the object. Use a rendered return when the program's final output is text.

Use the Typed Result

Replace the final expression of review.kedi with this block; keep its directives, type and procedure definitions above it:

[review: Review] = `review_change(args.title, args.diff_summary)`
[next_step: str] = Request another revision

> if: `review.decision == "approve"`:
  [next_step] := Queue for human review

= <next_step>: <`review.summary`>

This branch performs a Python comparison, not another model judgment. The trailing : makes that explicit. = initializes next_step; := updates its existing outer binding from the branch's child scope. Neither branch publishes or merges anything. For natural-language conditions and loop/map dataflow, see Control Flow and Loops and Map.

Pass Command-Line Arguments

Application flags belong after the Kedi source:

kedi review.kedi --title "Update parser" --diff-summary "Adds package syntax"

Dashed names become underscore attributes, so --diff-summary is available as args.diff_summary. Flags without values become booleans. Kedi's own options, such as --adapter and --test, are parsed by the CLI rather than exposed as application arguments.

Parse Before Running

kedi parse review.kedi

Parsing catches malformed syntax, duplicate selective imports, invalid directives, and other structural errors. It cannot prove that provider credentials exist or that a runtime-computed type is valid. Those checks happen during compilation or execution.

Continue the Program

  • Add deterministic checks with Testing.
  • Move reusable procedures into Modules.
  • Call the same runtime from Python.
  • Add Tools only when external evidence or actions are needed; a template alone does not fetch files or browse the web.