GNEOL Program Format Reference
A .gneol file defines a program for the Gneol platform. This document covers everything you need to write, deploy, and understand .gneol programs.
Installation
Quick install (recommended)
curl -sSL https://raw.githubusercontent.com/Gneol/Gneol/main/install.sh | bash
Install a specific version
curl -sSL https://raw.githubusercontent.com/Gneol/Gneol/main/install.sh | bash -s -- --version v0.3.0
Manual download
Download from the Releases page.
VS Code extension
Get syntax highlighting, autocomplete, and documentation for
.gneol files with the official
Gneol IntelliSense
extension for Visual Studio Code.
code --install-extension Gneol.gneol-intellisense
Getting Started
A minimal .gneol program is just a header:
program("Hello World")
.model("fast-llm")
gneol model).Save it as hello.gneol, then deploy:
gneol deploy -f hello.gneol -i
This deploys the program and starts an interactive chat session. To watch the file and redeploy automatically on every save, use -w instead:
gneol deploy -f hello.gneol -w
Then talk to your agent:
gneol # start an interactive session in the current directory
gneol -a # list and select an existing agent, then start chatting
That creates an agent named after your program title. Agents are the workers of Gneol: they receive messages, call functions, and execute triggers. From here you can add models, subagents, triggers, tools, and MCP servers as described below.
Architecture
Gneol runs as a local background server. It holds all runtime state — souls, programs, models, tools, triggers, event buckets, and sessions — under ~/.gneol, and listens on a local port (default 3999). The gneol terminal command is a lightweight client that connects to that server. Keeping state in one place is what makes Gneol's system of cooperating agents work.
Start the server explicitly:
gneol start # start the local server (foreground)
gneol start -d # start it in the background and detach
gneol start -p 4000 # listen on a custom port instead of 3999
Check whether it is up with gneol status, and shut it down with gneol stop.
Programs outlive the terminal. Once deployed, a program runs inside the detached runtime — closing your terminal does not stop your agents, scheduled triggers, or event listeners. Programs only stop when you ask them to, or when the machine they run on shuts down.
gneol stop -f hello.gneol # stops the program
gneol stop # stops the runtime
Nothing is registered to auto-start on reboot, so after a restart bring the runtime back with gneol start -d — or simply run any gneol command, which starts it for you if it is not already listening.
With the server running, calling gneol on its own starts an interactive session in the current directory.
Programs
When you run gneol in a directory that has no index.gneol, it asks how you would like to proceed and can create a starter program there. To deploy a program under any other name, point deploy at the file:
gneol deploy -f custom.gneol # deploy a program with any name
Program Model
Gneol programs describe a system of cooperating agents. For the client/server runtime that hosts them, see Architecture.
- Program — a
.gneolfile. Defines models, subagents, triggers, tools, and MCP connections. - Agent — a worker spawned from the program header. Each agent has a name, traits, backstory, workspace, and a model.
- Subagent — a specialized child agent declared with
subagent("Name"). Useful for delegating focused tasks (CI monitoring, UI debugging, etc.). - Trigger —
at()(time-based) oron()(event-based) blocks that fire actions automatically. - Tool — a function an agent can call. Built-ins come from the platform; custom ones come from
tool()scripts or MCP servers. - MCP Server — an external Model Context Protocol server whose tools become callable by agents.
A deployed program creates a root agent. It can dispatch work to its subagents (e.g., via trigger routing with .sentinel("Name")).
Path Resolution
All relative paths in a .gneol file resolve relative to the directory containing that .gneol file — not the process CWD and not the agent workspace.
This applies to:
.context("Label").resource("path")import("path").script("path")intool().config("path")inmcp().ifExec("path", ...)inat()/on()
Absolute paths and URLs are used as-is.
Installation & Deployment
Programs are deployed with the Gneol CLI. Start the runtime first if it isn't already running:
gneol start # start the runtime (--daemonize to run in background)
gneol deploy -f path/to/file.gneol # deploy once
gneol deploy -f path/to/file.gneol -i # deploy, then start an interactive session
gneol deploy -f path/to/file.gneol -w # watch the file and redeploy on save
deploy reconciles runtime state with your .gneol script:
- Parses the
.gneolfile and itsimport()s. - Creates/updates the root agent for the program.
- Applies model bindings, subagent declarations, and triggers.
- Loads custom
tool()scripts. - Connects
mcp()servers and syncs their tools. - Rebuilds the agent's function list so new tools are immediately available.
Use -i to jump straight into a chat after deploying, or -w to watch your .gneol file and auto-redeploy every time you save.
Other useful commands
| Command | Purpose |
|---|---|
gneol agent list | List agents on the local runtime. |
gneol agent scaffold <name> | Generate an agent scaffold as a .gneol snippet. |
gneol model list | List models on the local runtime. |
gneol mcp list | List MCP servers. |
gneol tool list | List tools. |
gneol event list | List events. |
gneol schedule list | List scheduled triggers. |
gneol secrets <action> [key] [value] | Manage encrypted API key store (list, get, set, delete). |
gneol convert | Convert .gneol to/from JSON or YAML. |
gneol rollback [target] | Rollback a previous deployment or resource change. |
gneol stop -f <file> | Stop a single program. |
gneol stop | Stop the whole runtime. |
gneol -a | List and select an existing agent, then start chatting. |
Run gneol --help for the full command list.
gneol secrets set <key> <value>, then reference them from your program with program().env(".env") or .model().apiKey("ENV_VAR") — no hard-coded keys in your .gneol file.Environment Variables
There are three ways to provide environment variables and API keys to your Gneol programs:
1. Global Secret Store (gneol secrets)
Store API keys and tokens globally, encrypted on your machine:
gneol secrets set OPENROUTER_API_KEY sk-...
gneol secrets list
gneol secrets get OPENROUTER_API_KEY
gneol secrets delete OPENROUTER_API_KEY
Reference keys by name in your .gneol file — no hard-coded secrets:
model("fast-llm")
.provider("openrouter")
.modelId("openai/gpt-4o-mini")
.apiKey("OPENROUTER_API_KEY")
2. Per-Program Env File
Point your program at a .env file on disk. Variables are injected into the program environment:
program("My App")
.env(".env")
If omitted, Gneol auto-detects a .env file in the program directory.
3. Shell Environment
Export variables in your terminal shell before running Gneol — available to all programs:
export OPENROUTER_API_KEY=sk-...
gneol deploy -f hello.gneol
Directives
1. program()
The program header. Every .gneol file must contain exactly one program() directive. import() directives may appear before it; all other directives must come after.
program("Title")
| Sub-function | Description |
|---|---|
.model("tag") | Assigns the parent agent's model tag. |
.use("agentId") | Binds the program to a specific existing agent. Usually auto-injected on install. |
.env("path") | Specifies an env file path (default: auto-detect). |
.context("Label").text("static text") | Injects static context into the system message. |
.context("Label").resource("path/or/url") | Loads file/URL content into the system message. |
.id("agentId") | Explicitly sets the agent ID. Unless you know what you're doing, let it be auto-injected. |
.avatar("path/or/url") | Sets the agent's avatar: a local .glb file path or an http(s):// URL pointing at a 3D model. Stored on the soul as avatarUri. Relative paths resolve against the program directory, preferring an avatar/ subfolder when the file lives there. |
program("My App")
.model("fast-llm")
.env(".env")
.context("Policy").text("Never share internal keys.")
.context("FAQ").resource("./docs/faq.md")
.avatar("src/avatar/Casual3_Female.glb")
GET /avatar?id=<soulId> — remote URLs get a 302 redirect, local .glb files are served directly as model/gltf-binary. Opening the avatar client with ?id=<soulId> loads the model and registers its animation clips, which enables Internal.fetchAvatarAnimations and Internal.animateAvatar.2. model()
Declares a model binding for a tag used by agents, subagents, or summarization.
model("tag")
| Sub-function | Description |
|---|---|
.provider("name") | The provider name (e.g., openrouter). Required if .apiKey() is set. |
.modelId("id") | The model identifier (e.g., openai/gpt-4o). |
.apiKey("ENV_VAR") | Env variable name holding the API key. |
.temperature(0.7) | 0.0 to 1.0. |
.maxTokens(4000) | Max output tokens. |
.rateLimit(10) | Requests per minute. |
provider and apiKey must always be paired.model("deepseek-v4-flash")
.provider("openrouter")
.modelId("deepseek/deepseek-chat")
.apiKey("DEEPSEEK_API_KEY")
.temperature(0.7)
.maxTokens(4000)
model("eagle-eye")
.provider("openrouter")
.modelId("openai/gpt-4o")
.apiKey("OPENROUTER_API_KEY")
.temperature(0.2)
.maxTokens(4000)
3. subagent()
Declares a specialized child agent. (The older sentinel() keyword is deprecated — use subagent() directly in new programs; the example below shows the modern form.)
subagent("Name")
| Sub-function | Description |
|---|---|
.model("tag") | Assigns a model tag (empty name = wildcard for all). |
.id("agent_id") | Assigns an immutable identifier (used for lookups on re-parse). |
.description("text") | Sets the subagent's specialty/description (updates on re-parse). |
.backstory("text") | Sets the subagent's persona/instructions. |
All sub-functions are optional per subagent.
subagent("Marcus")
.model("eagle-eye")
.description("CI monitoring specialist")
.backstory("You are a CI sentinel. Watch pipelines and report failures.")
A wildcard declaration applies its model to all subagents that don't declare one:
subagent().model("deepseek-v4-flash")
4. summarization()
Configures how conversations are summarized for the program's agents.
summarization()
| Sub-function | Description |
|---|---|
.model("tag") | Model tag to use for summarization. |
.prompt("...") | Summarization prompt (inline text, file path, or URL). |
summarization()
.model("fast-llm")
.prompt("tl;dr in 3 bullets")
5. import()
Recursively resolves another .gneol file and inlines its contents. Imported files should be fragments: they may contain any directive except another program() header (headers and sub-directives are stripped on import). Circular imports are detected and skipped. Imports can appear anywhere in the file.
import("common.gneol")
// common.gneol — a fragment with shared declarations
model("shared-model")
.provider("openai")
.modelId("gpt-4o-mini")
.apiKey("OPENAI_API_KEY")
6. at() — Time-Based Triggers
Schedules an action to run at specific times or intervals.
at("timeExpr")
Supported Time Expressions
| Expression | Meaning |
|---|---|
+10s / +5min / +2h / +1d | Relative from deployment time. |
9am / 3pm / 9:40am | Absolute 12-hour (agent's local timezone). |
14:30 / 18:00 | Absolute 24-hour (agent's local timezone). |
every:30mins / every:1h | Recurring interval. |
every:Mon / every:Tuesday | Day of week. |
every:1st / every:15th | Day of month. |
Fires are executed while the runtime is running; a missed fire while the runtime is down is not retroactively queued.
Sub-functions
| Sub-function | Description |
|---|---|
.max(N) | Max triggers before auto-stop. |
.do("message") | Action message sent to the agent (or routed subagent). Chainable: .do("a").do("b"). |
.resource("path") | Attach file/URL context to the action. |
.if("condition text") | LLM-evaluated condition; action only runs if true. |
.ifExec("script", "operator", "expected") | Runs a shell script and compares stdout to expected. Operators: eq (default), lt, gt, contains. Two-arg form uses eq. |
.sentinel("name") | Route the action to a subagent (deprecated alias). |
.modify() | Tag for review. |
at("every:10min")
.if("Backend health endpoint is reachable")
.do("Run health probe")
.max(1)
at("every:5min")
.ifExec("./scripts/health.sh", "ok")
.do("Health check passed.")
.max(3)
at("every:1min")
.ifExec("./scripts/cpu.sh", "gt", "80")
.do("CPU threshold exceeded.")
.max(5)
7. on() — Event-Based Triggers
Fires when an event is received. Events are pushed to the agent via the platform; see Built-in Events. (The .sentinel("name") routing option below is a deprecated alias for routing to a subagent — use the subagent's name directly.)
on("eventName")
| Sub-function | Description |
|---|---|
.maxSize(N) | Max pending jobs before auto-fire (fires once threshold reached). |
.delay(N) | Debounce window in seconds (coalesces rapid events). |
.do("message") | Action message. Chainable: .do("a").do("b"). |
.resource("path") | Attach file/URL context. |
.if("condition text") | LLM-evaluated condition. |
.sentinel("name") | Route to a subagent (deprecated alias). |
.modify() | Tag for review. |
.max(N) | Max triggers before auto-stop (global limit). |
on("deploy-event")
.maxSize(1)
.delay(10)
.if("Latest CI passed")
.do("Run deployment.")
.sentinel("Marcus")
8. tool() — Custom Tool Scripts
Declares a custom tool module from a worker script.
tool("name")
| Sub-function | Description |
|---|---|
.script("path") | Path to the worker script (relative or absolute). |
.description("text") | Human-readable description of the module. |
Worker scripts are written with the Gneol SDK (gneol-sdk, npm). See Programmatic SDK — Custom tool scripts for how to define tools.
After install, tools are available as tool.<ModuleName>.<toolName>(). Action logs from action() are forwarded to the session stream.
tool("CustomTool")
.script("./custom_tool.js")
9. mcp() — MCP Server Declarations
Connects MCP servers and syncs their tools into the agent's function list. mcp("name").config(...) uses the standard MCP config JSON format.
mcp("name")
| Sub-function | Description |
|---|---|
.config("path/or/url") | Local JSON config file path, or URL to a remote config. |
.token("ENV_VAR") | Chainable. Adds auth tokens: first token becomes Authorization: Bearer (HTTP) or TOKEN env (STDIO); extras become X-Token-2 (HTTP) / TOKEN_2 (STDIO). |
Config format:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
},
"exa": {
"url": "https://mcp.exa.ai/mcp"
}
}
}
Supports stdio (command/args) and HTTP Streamable (url) transports. Optional env/headers fields are supported; ${VAR} placeholders are expanded from the environment.
mcp("test-servers")
.config("mcp-servers.json")
mcp("secure-api")
.config("mcp-remote.json")
.token("MY_API_TOKEN")
Built-in Events
| Event | Fired when |
|---|---|
Idle | The agent is idle (no pending work). Often used for keep-alive checks. |
deploy-event | A program is deployed/redeployed (useful for CI-triggered deployments). |
message | A user or agent message arrives. |
Custom application events can be emitted by tools/scripts; on("name") listens for any event pushed to the agent.
Troubleshooting
Deploy fails with "missing model binding"
Every .model("tag") reference in the header/subagents must have a matching model("tag") block, or the tag must already be cached.
MCP tools not appearing
Ensure mcp() config paths are correct, then restart the runtime after deploy; HTTP servers may require network access. Tools appear prefixed with the declared MCP name: mcp.<name>.<toolName> (e.g., mcp.test-servers.<toolName>).
Trigger never fires
Check the at() / on() conditions. LLM-evaluated .if() can fail silently; try removing it to isolate.
ifExec not matching
Confirm the script is executable and its stdout exactly matches (or use contains). Paths resolve relative to the .gneol file.
Subagent not receiving routed actions
Verify the subagent name matches a subagent("Name") declaration exactly.
Complete Example
program("Dispatch Test")
.model("deepseek-v4-flash") // ensure a model("deepseek-v4-flash") block exists or the tag is cached
.env(".env")
.context("Policy").text("Never share internal keys.")
.context("FAQ").resource("./docs/faq.md")
.avatar("src/avatar/Casual3_Female.glb")
model("deepseek-v4-flash")
.provider("openrouter")
.modelId("deepseek/deepseek-chat")
.apiKey("DEEPSEEK_API_KEY")
.temperature(0.7)
.maxTokens(4000)
model("eagle-eye")
.provider("openrouter")
.modelId("openai/gpt-4o")
.apiKey("OPENROUTER_API_KEY")
.temperature(0.2)
.maxTokens(4000)
subagent("Marcus")
.model("eagle-eye")
.description("CI sentinel")
subagent().model("deepseek-v4-flash")
summarization().model("deepseek-v4-flash").prompt("tl;dr in 3 bullets")
tool("CustomTool")
.script("./custom_tool.js")
mcp("test-servers")
.config("mcp-servers.json")
at("every:10min")
.if("Context feature is fully working")
.do("Log: Context feature is done.")
.max(1)
on("deploy-event")
.maxSize(1)
.if("Latest CI passed")
.do("Run deployment.")
at("every:5min")
.ifExec("./scripts/health.sh", "ok")
.do("Health check passed.")
.max(3)
at("every:1min")
.ifExec("./scripts/cpu.sh", "gt", "80")
.do("CPU threshold exceeded.")
.max(5)
Programmatic SDK (gneol-sdk)
The gneol-sdk package builds .gneol programs from TypeScript or JavaScript. Instead of editing script text by hand, you compose the program through typed accessors — models, subagents, triggers, tools, MCP servers, summarization, imports — then write it to disk and deploy it with the Gneol CLI.
Prerequisites
- Node.js v16 or later.
- Gneol CLI on your
PATH— the SDK shells out to it forsave()anddeploy(). Gneol ships as a single self-contained binary (no npm package):
curl -sSL https://raw.githubusercontent.com/Gneol/Gneol/main/install.sh | bash
Installation
npm install gneol-sdk
Quick start
Import Program, point it at the .gneol file you want to build, populate it, then save() and deploy():
import { Program } from "gneol-sdk";
// 1. Point the Program at a .gneol file (created on first save)
const program = new Program("./support-triage.gneol");
// 2. Set the program header
program.header.set({ name: "Support Triage", model: "fast-llm" });
// 3. Declare a model binding
await program.models.create({
name: "fast-llm",
provider: "openrouter",
modelId: "openai/gpt-4o-mini",
apiKey: "OPENROUTER_API_KEY",
temperature: 0.1,
maxTokens: 1024,
});
// 4. Declare a subagent
await program.sentinels.create({
name: "BillingBot",
model: "fast-llm",
description: "Handles payment, refund, and invoice inquiries.",
});
// 5. Route a scheduled trigger to it
await program.triggers.create({
timeExpr: "every:30min",
do: ["Run escalation check for tickets older than 4 hours."],
sentinel: "BillingBot",
max: 48, // 48 runs x 30min = 24 hours of coverage
});
// 6. Write the .gneol file, then deploy it
await program.save(); // → ./support-triage.gneol
await program.deploy(); // → gneol deploy -f ./support-triage.gneol
Directive accessors
Every directive in this reference is an accessor on the Program instance, so you create, list, update, and delete them as objects instead of editing script text by hand. The generated output is the same plain-text .gneol format documented above.
| Directive | Accessor | Methods |
|---|---|---|
| Header | program.header | .set(), .get(), .clear() |
| Model | program.models | .create(), .get(), .list(), .update(), .delete(), .clear() |
| Subagent | program.sentinels | .create(), .get(), .list(), .update(), .delete(), .clear() |
| Summarization | program.summarizations | .create(), .get(), .list(), .update(), .delete(), .clear() |
| Tool | program.tools | .create(), .get(), .list(), .update(), .delete(), .clear() |
| MCP server | program.mcp | .create(), .get(), .list(), .update(), .delete(), .clear() |
Trigger (at() / on()) | program.triggers | .create(), .get(), .list(), .update(), .delete(), .clear() |
| Import | program.imports | .add(), .remove(), .list(), .clear() |
Every accessor returns the new resource id as a string from .create(), and .list() returns each item with its resourceId attached.
Triggers, MCP, tools, and imports
// Event-driven trigger (generates on()) with debounce and a pending-job threshold
await program.triggers.create({
eventName: "new-ticket",
do: ["Classify ticket by category.", "Route to the appropriate subagent."],
maxSize: 10, // fire once 10 pending jobs accumulate
delay: 5, // debounce 5 seconds
});
// Daily scheduled trigger (generates at())
await program.triggers.create({ timeExpr: "9am", do: ["Generate daily support summary."] });
// MCP server — config is the MCP config JSON as a string
await program.mcp.create({
name: "account-db",
config: JSON.stringify({ type: "http", url: "https://internal-api.example.com/mcp" }),
});
// Custom tool — points at a worker script (see below)
await program.tools.create({
name: "ticket-lookup",
script: "./tools/ticket-lookup.js",
description: "Search and retrieve customer support tickets by ID or email.",
});
// Summarization
await program.summarizations.create({
model: "summary-model",
prompt: "Summarize all resolved tickets in the last 24 hours.",
});
// Shared .gneol fragments
program.imports.add("shared/escalation-policies.gneol");
Naming: subagents and sentinels
The SDK keeps the original TypeScript names: the accessor is program.sentinels and the trigger routing field is sentinel. In the generated .gneol text both appear as subagent(), matching the directive documented in this reference — so program.sentinels.create({ name: "BillingBot", ... }) plus { sentinel: "BillingBot" } on a trigger produces subagent("BillingBot") and routes the action to it.
Custom tool scripts
The script referenced by tool() .script() (see tool() — Custom Tool Scripts) is a worker module built with the SDK's Module class. Each .tool() definition declares its input and output schemas with zod, plus optional action (a log line forwarded to the session stream) and auth (permission gate) hooks.
import { Module, z } from "gneol-sdk";
const mod = new Module("TicketLookup");
mod.tool({
name: "lookupTicket",
description: "Retrieve a support ticket by ID.",
parameters: z.object({ ticketId: z.string() }),
output: z.object({ subject: z.string(), status: z.string() }),
auth: async () => true,
action: async (args) => `lookupTicket ${args.ticketId}`,
func: async (args) => ({ subject: "Refund not received", status: "open" }),
});
mod.parse(); // starts the worker message loop
Registered tools are callable by the agent as tool.<ModuleName>.<toolName>() — for example tool.TicketLookup.lookupTicket().
Server and Module
Beyond program authoring, the package exports Server (hosts one or more Module instances over a socket) and execAsync (a promise wrapper around shell execution).
import { Server, Module } from "gneol-sdk";
const server = new Server(); // new Server(url?, scid?)
const events = await server.serve(4000, [modA, modB]);
await server.close();
What is on disk
A Program holds its directives in memory. save() serializes them and shells out to gneol convert to write the .gneol file at the path given to the constructor. deploy(outputPath?) does the same conversion — optionally to a different path — and then runs gneol deploy -f <path>.
Keep your SDK source and the generated .gneol file under version control (for example git). To roll back, check out the previous version and re-run program.deploy().