You declare what your app is made of โ its services, its routes, its data, what runs when something comes in โ and Telo wires it together and runs it. Control flow and the values passing between resources are declared too, and checked before anything starts.
Whole applications
On the left, everything the application is. On the right, that application running.
- HTTP API
- Database
- Batch
- Config
- Library
- Tests
- Transactions
A server, a route with a validated response, and the handler behind it.
kind: Telo.Application
metadata:
name: GreetingApi
imports:
Http: oci://ghcr.io/telorun/http-server@0.39.0
Run: oci://ghcr.io/telorun/run@0.29.0
targets:
- !ref Server
ports:
http: { env: PORT, default: 8080 }
---
kind: Http.Server
metadata:
name: Server
listen:
port: !cel "ports.http"
mounts:
- path: /v1
mount: !ref Api
---
kind: Http.Api
metadata:
name: Api
routes:
- request: { path: /greet, method: GET }
handler: !ref Greet
returns:
- status: 200
content:
application/json:
body:
message: !cel "result.message"
---
kind: Run.Value
metadata:
name: Greet
outputType:
kind: Telo.JsonSchema
schema:
type: object
required: [message]
properties:
message: { type: string }
value:
message: Hello!
$ telo ./telo.yaml
Server listening at http://localhost:8080
$ curl localhost:8080/v1/greet
{"message":"Hello!"}
The same shape with somewhere to keep things. Values in !sql bind as parameters, so a quote in the input is a quote, never SQL.
kind: Telo.Application
metadata:
name: NotesApi
imports:
Http: oci://ghcr.io/telorun/http-server@0.39.0
Sql: oci://ghcr.io/telorun/sql@0.28.1
SQLite: oci://ghcr.io/telorun/sqlite@0.11.0
targets:
- !ref Migrate
- !ref Server
ports:
http: { env: PORT, default: 8080 }
---
kind: SQLite.Connection
metadata:
name: Db
file: ./notes.db
---
kind: SQLite.Table
metadata:
name: Notes
table: notes
columns:
id: { type: integer, primaryKey: true, identity: always }
title: { type: text, nullable: false }
body: { type: text, nullable: false }
---
kind: SQLite.Schema
metadata:
name: Migrate
connection: !ref Db
tables:
- !ref Notes
---
kind: Sql.Command
metadata:
name: InsertNote
connection: !ref Db
---
kind: Sql.Query
metadata:
name: ListNotes
connection: !ref Db
---
kind: Http.Server
metadata:
name: Server
listen:
port: !cel "ports.http"
mounts:
- path: /v1
mount: !ref Notes
---
kind: Http.Api
metadata:
name: Notes
routes:
- request:
path: /notes
method: POST
schema:
body:
type: object
required: [title, body]
properties:
title: { type: string, minLength: 1 }
body: { type: string }
handler: !ref InsertNote
inputs:
sql: !sql "INSERT INTO notes (title, body)
VALUES (${{ request.body.title }}, ${{ request.body.body }})"
returns:
- status: 201
content:
application/json:
body:
created: !cel "result.rowCount"
- request: { path: /notes, method: GET }
handler: !ref ListNotes
inputs:
sql: "SELECT id, title, body FROM notes ORDER BY id"
returns:
- status: 200
content:
application/json:
body: !cel "result.rows"
$ telo ./telo.yaml
Server listening at http://localhost:8080
$ curl -X POST localhost:8080/v1/notes \
-H 'content-type: application/json' \
-d '{"title":"first","body":"hello"}'
{"created":1}
$ curl localhost:8080/v1/notes
[{"id":1,"title":"first","body":"hello"}]
No server at all โ a one-shot task that reads, aggregates, reports, and exits.
kind: Telo.Application
metadata:
name: SalesReport
imports:
Fs: oci://ghcr.io/telorun/fs@0.15.0
Collection: oci://ghcr.io/telorun/collection@0.9.0
Run: oci://ghcr.io/telorun/run@0.29.0
Console: oci://ghcr.io/telorun/console@0.19.0
targets:
- !ref Report
---
kind: Run.Sequence
metadata:
name: Report
steps:
- name: Load
invoke: { kind: Fs.File }
inputs:
path: ./orders.json
- name: Sum
invoke:
kind: Collection.Summarize
collection: !cel "inputs.orders"
aggregate:
orders: !cel "size(group)"
revenue: !cel "sum(group.map(o, o.total))"
inputs:
orders: !cel "parseJson(steps.Load.result.content)"
- name: Print
invoke: !ref Console.writeLine
inputs:
output: !cel "string(steps.Sum.result.summary.orders) + ' orders, '
+ string(steps.Sum.result.summary.revenue) + ' revenue'"
$ cat orders.json
[
{ "region": "eu", "total": 120.5 },
{ "region": "us", "total": 80.0 },
{ "region": "eu", "total": 45.25 }
]
$ telo ./telo.yaml
3 orders, 245.75 revenue
A one-shot task configured by the environment. Greeting declares the shape it returns, so the step that reads it is type-checked against that shape โ a misspelled field is caught with the spelling you wanted.
kind: Telo.Application
metadata:
name: Welcome
imports:
Run: oci://ghcr.io/telorun/run@0.29.0
Console: oci://ghcr.io/telorun/console@0.19.0
variables:
name:
env: NAME
type: string
default: World
targets:
- !ref Main
---
kind: Run.Value
metadata:
name: Greeting
outputType:
kind: Telo.JsonSchema
schema:
type: object
required: [message]
properties:
message: { type: string }
value:
message: !cel "'Hello, ' + variables.name + '!'"
---
kind: Run.Sequence
metadata:
name: Main
steps:
- name: Build
invoke: !ref Greeting
- name: Say
invoke: !ref Console.writeLine
inputs:
output: !cel "steps.Build.result.message"
$ NAME=Telo telo ./telo.yaml
Hello, Telo!
# Now misspell it: steps.Build.result.mesage
$ telo check ./telo.yaml
telo.yaml:38:20 error Run.Sequence/Main:
CEL at 'steps[1].inputs.output':
'steps.Build.result.mesage' is not defined
(available: message)
CEL_UNKNOWN_FIELD
1 error
A Telo.Library is an importable unit โ kinds, or ready-made instances like this one. Importing it is a relative path: no registry, no publish step, no build. Everything under exports: is what importers may reach; anything else stays private to the library.
kind: Telo.Library
metadata:
name: Greeting
imports:
Run: oci://ghcr.io/telorun/run@0.29.0
exports:
resources:
- Formal
---
kind: Run.Value
metadata: { name: Formal }
inputType:
kind: Telo.JsonSchema
schema:
type: object
required: [name]
properties:
name: { type: string }
outputType:
kind: Telo.JsonSchema
schema:
type: object
required: [message]
properties:
message: { type: string }
bindings:
who: !cel "inputs.name"
value:
message: !cel "'Good evening, ' + who + '.'"
kind: Telo.Application
metadata:
name: Welcome
imports:
# A relative path. The library is loaded from disk, and its exported
# instances are reachable as !ref Greeting.<name>.
Greeting: ./greeting
Console: oci://ghcr.io/telorun/console@0.19.0
Run: oci://ghcr.io/telorun/run@0.29.0
targets:
- !ref Main
---
kind: Run.Sequence
metadata: { name: Main }
steps:
- name: Evening
invoke: !ref Greeting.Formal
inputs: { name: Ada }
- name: Say
invoke: !ref Console.writeLine
inputs:
output: !cel "steps.Evening.result.message"
$ telo ./telo.yaml
Good evening, Ada.
A test is an application. It includes the same routes the app serves, stands a real server up in a with: scope โ a real socket, a real HTTP call, torn down when the sequence ends โ and asserts against a plain expected value. No test framework and no mocks; the kernel that runs it is the one that runs production.
# The routes, in a partial file the application and its test both include โ
# so the test exercises the real thing rather than a copy of it.
kind: Http.Api
metadata: { name: Api }
routes:
- request: { path: /greet, method: GET }
handler: !ref Greet
returns:
- status: 200
content:
application/json:
body:
message: !cel "result.message"
---
kind: Run.Value
metadata: { name: Greet }
outputType:
kind: Telo.JsonSchema
schema:
type: object
required: [message]
properties:
message: { type: string }
value:
message: Hello!
kind: Telo.Application
metadata:
name: GreetApiTest
imports:
Http: oci://ghcr.io/telorun/http-server@0.39.0
HttpClient: oci://ghcr.io/telorun/http-client@0.23.0
Run: oci://ghcr.io/telorun/run@0.29.0
Assert: oci://ghcr.io/telorun/assert@0.21.1
include: [ ../api.yaml ]
logging:
level: warn
targets:
- !ref Test
---
kind: Run.Sequence
metadata: { name: Test }
# Created when the sequence starts, torn down when it ends โ so the
# port is freed and the process exits on its own.
with:
- kind: Http.Server
metadata: { name: Server }
listen:
address: 127.0.0.1
port: 8973
mounts:
- { path: /v1, mount: !ref Api }
targets:
- !ref Server
steps:
- name: Call
invoke:
kind: HttpClient.Request
url: http://127.0.0.1:8973/v1/greet
method: GET
- name: Check
invoke: { kind: Assert.Equals }
inputs:
actual:
status: !cel "steps.Call.result.status"
body: !cel "steps.Call.result.body"
expected:
status: 200
body: { message: Hello! }
$ telo ./tests/greet-api.yaml
Assert.Equals.SequenceTestSteps1Check: assertion passed
โ {"status":200,"body":{"message":"Hello!"}}
# Now change the expectation to "Hei!" and run it again.
$ telo ./tests/greet-api.yaml
Assert.Equals.SequenceTestSteps1Check: assertion failed
โ expected {"status":200,"body":{"message":"Hei!"}},
got {"status":200,"body":{"message":"Hello!"}}
tests/greet-api.yaml error ERR_ASSERTION_FAILED
1 error (exit 1)
Move declares that it must run inside a transaction. That is not a comment โ telo check walks every path that can reach it and refuses the ones that would run it loose.
kind: Telo.Application
metadata:
name: Ledger
imports:
Sql: oci://ghcr.io/telorun/sql@0.28.1
SQLite: oci://ghcr.io/telorun/sqlite@0.11.0
Run: oci://ghcr.io/telorun/run@0.29.0
targets:
- !ref Migrate
- !ref Main
---
kind: SQLite.Connection
metadata:
name: Db
file: ./ledger.db
---
kind: SQLite.Table
metadata:
name: Accounts
table: accounts
columns:
name: { type: text, primaryKey: true }
balance: { type: integer, nullable: false }
---
kind: SQLite.Schema
metadata:
name: Migrate
connection: !ref Db
tables:
- !ref Accounts
migrations:
0002_seed_accounts:
statement: |
INSERT OR IGNORE INTO accounts (name, balance)
VALUES ('alice', 100), ('bob', 0)
---
kind: Run.Sequence
metadata:
name: Main
steps:
- name: Send
invoke: !ref Transfer
inputs:
amount: 10
---
# Both writes land, or neither does.
kind: Sql.Transaction
metadata:
name: Transfer
connection: !ref Db
steps: !ref TransferSteps
inputs:
amount: !cel "inputs.amount"
---
kind: Run.Sequence
metadata:
name: TransferSteps
steps:
- name: Debit
invoke: !ref Move
inputs:
sql: !sql "UPDATE accounts SET balance = balance - ${{ inputs.amount }}
WHERE name = 'alice'"
- name: Credit
invoke: !ref Move
inputs:
sql: !sql "UPDATE accounts SET balance = balance + ${{ inputs.amount }}
WHERE name = 'bob'"
---
# No connection: โ it is reached through the transaction it declares.
kind: Sql.Command
metadata:
name: Move
transaction: !ref Transfer
$ telo check ./telo.yaml
โ No issues found
# Now run the statement outside the transaction โ
# add "- !ref Move" to targets: and check again.
$ telo check ./telo.yaml
telo.yaml:11:10 error Sql.Command 'Move' requires a
SQL.Transaction zone on SQLite.Connection 'Db', and the path
Move โ Ledger.targets[2] reaches the application's boot targets,
which nothing encloses. the statement would execute outside any
transaction. ZONE_REQUIREMENT_UNSATISFIED
1 error
Build the first one step by step โ ยท What else is caught before it runs โ