Skip to content

Procedures

Procedures package Kedi statements into typed, reusable callables. They are also the unit exposed to tools, tests, evals, optimization, generated code, and the Python API.

Define and Call a Procedure

Declare a procedure with @name(parameters): and indent its body:

@greet(name: str) -> str:
  = Hello, <name>.

= <greet(Ada)>

The name must be a valid Python identifier. The body can contain assignments, templates, Python blocks, directives, nested types, nested procedures, and a return. Call it with <greet(Ada)> in rendered Kedi text or greet("Ada") in a Python expression.

Procedure Bodies and Returns

Statements execute in source order. A return statement starts with =:

@summarize(text: str) -> str:
  >> One-sentence summary of <text>: [summary: str].
  = <summary>

Execution stops at the first reached return. If an untyped procedure reaches the end without a value, it returns an empty string. For non-string behavior, add an explicit return annotation and return a native value.

Keep deterministic transformation in Python and model judgement in >> templates. A procedure may combine both, but its name and return type should still describe one coherent operation.

Local Variables

Parameters and assignments belong to the procedure invocation:

[prefix] = global

@format(value: str) -> str:
  [prefix] = local
  [result] = <prefix>: <value>
  = <result>

= <format(item)> / <prefix>

The procedure returns "local: item" while the top-level prefix remains "global". New local names do not leak to callers.

Parameters are read-only bindings for lexical assignment purposes. Reuse a new local name when deriving from a parameter rather than treating the parameter as shared mutable state.

Calling Other Procedures

Procedures can form pipelines:

@normalize(value: str) -> str:
  = `value.strip().lower()`

@classify(value: str) -> str:
  >> The category of <value> is [category: str].
  = <category>

@process(value: str) -> str:
  [normalized: str] = <normalize(<value>)>
  = <classify(<normalized>)>

A sole call on an assignment right-hand side preserves the called procedure's native return type. Text around the call renders the assignment as str.

Nested Procedures

A procedure can declare helpers visible only during that invocation:

@invoice_total(lines: list[float]) -> float:
  @subtotal(values: list[float]) -> float:
    = `sum(values)`

  = `round(subtotal(lines), 2)`

The nested subtotal is not visible after invoice_total returns. A nested definition shadows an outer procedure with the same name only inside its lexical scope.

Use nested procedures for helpers that are meaningful only to one operation or that need to capture invocation state. Use a top-level procedure when several features should call it or when it should be exported, tested, optimized, or exposed as a tool independently.

Lexical Closures

Nested procedures capture the surrounding invocation and observe the latest outer value:

@make_report(title: str) -> str:
  [state] = draft
  @render() -> str:
    = <title>: <state>
  [state] = approved
  = <render()>

The result is "title [approved]", not "title [draft]". Closure lookup walks the current local scope, outer procedure scopes, imports, and top-level globals. A nested parameter shadows a captured name.

Assignments made through nested lexical scopes can update a captured Kedi binding. This is different from Python block rules, where rebinding from a nested Python function requires global.

Recursion

Compiled procedures are ordinary callable values and can be called from Python, including recursively:

@factorial(n: int) -> int:
  = `1 if n <= 1 else n * factorial(n - 1)`

= `factorial(5)`

Use recursion only when it improves the domain model. Iteration in a Python block is usually clearer for large collections and avoids Python's recursion limit. Model-calling recursion can also multiply cost and latency; bound it explicitly.

Procedure Documentation

Place a ### block first in the body to define the procedure docstring:

@lookup_order(order_id: str) -> str:
  ###
  Return the canonical status for one order.

  Args:
    order_id: External order identifier.
  ###
  = pending

Kedi surfaces this text in editor hover, generated Python stubs, procedure JSON schemas, and tool descriptions. A block comment after another statement remains a comment and does not become the procedure docstring.

Structured Docstrings and JSON Schema

Kedi follows the summary-and-body convention from PEP 257 and recommends Google-style Python docstrings for structured Args: and Returns: sections. PEP 257 defines general docstring conventions but does not prescribe those section names; Args: and Returns: come from the Google-style convention.

When a procedure is exposed as a tool, Kedi parses the leading docstring and adds its descriptions to the generated JSON Schemas:

@quote_shipping(destination: str, weight_kg: float, express: bool = `False`) -> float:
  ###
  Calculate a shipping quote for one destination.

  Args:
      destination: Country or region receiving the shipment.
      weight_kg: Package weight in kilograms; must be greater than zero.
      express: Whether to use the faster express service.

  Returns:
      Estimated shipping price in US dollars.
  ###
  = `0.0`

The procedure signature and docstring produce this input schema:

{
  "additionalProperties": false,
  "properties": {
    "destination": {
      "title": "Destination",
      "type": "string",
      "description": "Country or region receiving the shipment."
    },
    "weight_kg": {
      "title": "Weight Kg",
      "type": "number",
      "description": "Package weight in kilograms; must be greater than zero."
    },
    "express": {
      "default": false,
      "title": "Express",
      "type": "boolean",
      "description": "Whether to use the faster express service."
    }
  },
  "required": ["destination", "weight_kg"],
  "title": "quote_shipping",
  "type": "object",
  "description": "Calculate a shipping quote for one destination. Returns `float` instance."
}

Kedi emits the return contract separately:

{
  "type": "number",
  "description": "Estimated shipping price in US dollars."
}

Types, required fields, and defaults come from the procedure signature. Descriptions under Args: are matched to parameters by name, while the Returns: text describes the return schema. Docstring text documents the contract; it does not override its types. Kedi also accepts common reStructuredText forms such as :param name: and :return:, but Google style is the recommended form for new code.

Document externally visible procedures and every procedure exposed with > tool:. Include semantic constraints that types cannot express; do not duplicate the signature verbatim.

Failure Behavior

Calling a procedure fails when arguments are missing or excessive, a native value violates a parameter annotation, the body raises, or the return violates its annotation. Kedi retains source spans across nested calls so the reported failure points back to the relevant .kedi statement.