Docs GitHub

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")
Note: model tags that are not declared in the file must already exist on the local runtime (e.g. pre-cached tags shipped with fresh installs or added via 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.

GNEOL SERVER local daemon · state in ~/.gneol · default port 3999
▲
gneolCLI / terminal

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
Note: the server owns the runtime state, so a deployment is applied there rather than in your local directory.

Program Model

Gneol programs describe a system of cooperating agents. For the client/server runtime that hosts them, see Architecture.

  • Program — a .gneol file. 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) or on() (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") in tool()
  • .config("path") in mcp()
  • .ifExec("path", ...) in at()/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:

  1. Parses the .gneol file and its import()s.
  2. Creates/updates the root agent for the program.
  3. Applies model bindings, subagent declarations, and triggers.
  4. Loads custom tool() scripts.
  5. Connects mcp() servers and syncs their tools.
  6. Rebuilds the agent's function list so new tools are immediately available.
Note: some sync steps (e.g., MCP connections) require a runtime restart to take full effect.

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

CommandPurpose
gneol agent listList agents on the local runtime.
gneol agent scaffold <name>Generate an agent scaffold as a .gneol snippet.
gneol model listList models on the local runtime.
gneol mcp listList MCP servers.
gneol tool listList tools.
gneol event listList events.
gneol schedule listList scheduled triggers.
gneol secrets <action> [key] [value]Manage encrypted API key store (list, get, set, delete).
gneol convertConvert .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 stopStop the whole runtime.
gneol -aList and select an existing agent, then start chatting.

Run gneol --help for the full command list.

API keys: store secrets with 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-functionDescription
.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")
Avatars: the model is served at 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-functionDescription
.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.
Validation: 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-functionDescription
.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-functionDescription
.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

ExpressionMeaning
+10s / +5min / +2h / +1dRelative from deployment time.
9am / 3pm / 9:40amAbsolute 12-hour (agent's local timezone).
14:30 / 18:00Absolute 24-hour (agent's local timezone).
every:30mins / every:1hRecurring interval.
every:Mon / every:TuesdayDay of week.
every:1st / every:15thDay 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-functionDescription
.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-functionDescription
.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-functionDescription
.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-functionDescription
.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

EventFired when
IdleThe agent is idle (no pending work). Often used for keep-alive checks.
deploy-eventA program is deployed/redeployed (useful for CI-triggered deployments).
messageA 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 for save() and deploy(). 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.

DirectiveAccessorMethods
Headerprogram.header.set(), .get(), .clear()
Modelprogram.models.create(), .get(), .list(), .update(), .delete(), .clear()
Subagentprogram.sentinels.create(), .get(), .list(), .update(), .delete(), .clear()
Summarizationprogram.summarizations.create(), .get(), .list(), .update(), .delete(), .clear()
Toolprogram.tools.create(), .get(), .list(), .update(), .delete(), .clear()
MCP serverprogram.mcp.create(), .get(), .list(), .update(), .delete(), .clear()
Trigger (at() / on())program.triggers.create(), .get(), .list(), .update(), .delete(), .clear()
Importprogram.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().

Read the full SDK guide on npm