Skip to main content

Libraries

The unit of reuse in Telo is the library. A Telo.Library is an importable manifest with its own scope, its own configuration contract, and an explicit list of what it lets importers see — the same shape whether it lives in a sibling directory or is published for the world.

A note on vocabulary, because Telo uses two words. Library is the kind you write (kind: Telo.Library). Module is the unit it becomes once published — what telo module versions inspects and what the hub indexes. They are not synonyms: an Application is a module too, with its own scope, but it is never a library because nothing can import it.

This page is about libraries: writing one, importing one, choosing what it exports, and publishing it when it outgrows your repo. It ends with include:, which splits a file without creating a boundary at all.

A library, and importing it​

A library's resources are invisible to the importer unless explicitly exported. Start one anywhere in your repo — no publishing, no registry, no build step:

# libs/greetings/telo.yaml
kind: Telo.Library
metadata:
name: Greetings
version: 1.0.0
description: Greeting text for one person, in a configurable language.
imports:
Run: oci://ghcr.io/telorun/run@0.28.0
variables:
greeting:
type: string
default: Hello
exports:
resources:
- Greeter
---
kind: Run.Value
metadata:
name: Greeter
inputType:
kind: Telo.JsonSchema
schema:
type: object
properties:
name: { type: string }
required: [name]
outputType:
kind: Telo.JsonSchema
schema:
type: object
properties:
message: { type: string }
required: [message]
value:
message: !cel "variables.greeting + ', ' + inputs.name + '!'"

Note the library declares variables: without an env: key. Only the root application reads the host environment; a library is given its values by whoever imports it (an env: inside a library is rejected outright with LIBRARY_ENV_KEY_REJECTED).

The importer supplies them and reaches the export by alias:

kind: Telo.Application
metadata:
name: GreetingApp
version: 1.0.0
imports:
Console: oci://ghcr.io/telorun/console@0.19.0
Run: oci://ghcr.io/telorun/run@0.28.0
Greetings:
source: ./libs/greetings
variables:
greeting: Hej
targets:
- !ref Main
---
kind: Run.Sequence
metadata:
name: Main
steps:
- name: Greet
invoke: !ref Greetings.Greeter # <Alias>.<exported name>
inputs:
name: World
- name: Print
inputs:
output: !cel "steps.Greet.result.message"
invoke: !ref Console.writeLine
Hej, World!

An import entry is either a bare source string (Console: oci://…) or the object form above when you need to pass variables: / secrets: — or supply the library's typed needs:, a database or a signing key it asks for (see Declaring a need). Either way the alias is yours to choose: it is the prefix on every kind and every cross-module reference in the importing file, so the library never dictates what you call it.

source: ./libs/greetings is a relative path — the library is read from disk. That is the whole setup. Nothing about the library changes when it later becomes a published module; only the importer's source: does.

Two things a library can export​

exports: has two lists, and they answer different questions.

exports.resources — ready-made instances​

"Here is a configured thing; use it." The consumer references it as !ref <Alias>.<name> and reads its published values as !cel "resources.<Alias>.<name>.<field>". Entries are plain name strings — the !ref tag is not used in the export list itself.

This is the tool for hiding wiring. A library that imports sql and sqlite, configures a connection, and exports just that instance leaves its consumer with one import instead of two:

exports:
resources:
- Db # a configured Sql.Connection, ready to use

exports.kinds — types the importer may instantiate​

"Here is a blueprint; make your own." The consumer writes kind: <Alias>.<Kind> and configures it themselves.

exports:
kinds:
- Greeter

Export a kind, an instance, or both. Omitting the kind while exporting an instance is how you enforce a singleton — consumers can use your configured resource but cannot construct another.

Inside the library, refer to your own kinds through the automatic Self alias (kind: Self.Greeter) — there is no import to alias against, and Self resolves your own kinds regardless of what you export.

Re-exporting​

Both lists accept <Alias>.<name> to pass something through from a library you yourself import:

exports:
resources:
- Domain.Db # re-export the instance my `Domain` import owns
kinds:
- Domain.Repository

Re-export is transitive to any depth, and every hop resolves to the same instance — not a copy.

Which one do I want?​

SituationUse
A reusable piece with its own configurationa Telo.Library in-repo, imported by relative path
Consumers should not have to know which backend you chosea library exporting the configured instance
Consumers need to declare many of these themselvesa library exporting the kind
Other teams or repositories need itpublish the library — Authoring a module
One app, one scope, files just getting longinclude: — see below

Publishing​

A library imported by relative path becomes a published module with no structural change — you pin it by ref instead of path:

imports:
Greetings: oci://ghcr.io/acme/greetings@1.2.0

metadata.description on the library and on each kind is what the hub indexes for search, so write it for someone who does not yet know your module exists. See Authoring a module and, for extending an existing kind rather than composing one, Kind inheritance.

When you do not want a boundary — include:​

Sometimes a file is simply long, and there is nothing to encapsulate. include: loads other files into the same module scope, as if you had pasted them in:

kind: Telo.Application
metadata:
name: MyApp
version: 1.0.0
include:
- ./routes/*.yaml
- ./handlers/*.yaml

The included files are partials: resource documents only, and they may not declare Telo.Application, Telo.Library, or Telo.Definition. Everything lands in one scope, so a !ref in one file resolves a resource declared in another with no ceremony, and the including file's imports: aliases are the ones a partial's kinds are written against.

This is not a library. There is no separate scope, no configuration contract, and nothing hidden — the split is for readability, and every name stays global to the module. Reach for it when one application's file has grown unwieldy; reach for a library the moment something should be reusable, configurable, or opaque from the outside.

One use worth knowing: a partial can be included by an application and by its test, so the test exercises the same declarations the app serves rather than a copy that can drift.

See also​