Idyllic prompt languageexecutable prompts that still read like prose
I built the Idyllic prompt language because long prompts were difficult to reuse and reason about. A prompt could mention tools, data, and several steps, but the text did not define which parts were calls, which context each call received, or how execution should branch.
I wanted enough structure to make those choices explicit. I also wanted the result to remain readable by people who already knew how to write instructions in prose. Turning the prompt into a conventional programming language would have defeated that purpose.
The result is an executable document. Each paragraph is one statement. Inline markers identify objects and modifiers. Structured blocks handle decisions and events. The document parses into an AST that a tree-walking interpreter executes.
A paragraph is the unit of execution
I began with the smallest rule that gave the runtime a stable boundary: one paragraph expresses one instruction.
Paragraphs already separate ideas in ordinary writing. Using them as statements meant writers did not need semicolons or one command per line. The interpreter could execute one complete intent at a time and record its input, output, and changes to context.
Within a paragraph, I added two inline constructs:
@mentionnames an object, function, or data source.#directivemodifies how the instruction should run.
They fit into sentences without reorganizing the prose around code syntax:
Compare @sleep with @productivity for last month #briefly.The mentions resolve to typed objects and operations. The directive changes execution metadata. The remaining words stay ordinary instructions for the model.
I rejected inline conditionals and expression syntax because they changed the document's center of gravity. Once a paragraph contains nested boolean expressions and braces, the prose becomes decoration around a program. Idyllic uses separate structured blocks only when the execution model requires them.
Decisions let the model choose a branch
Prompts still need branching. Many branches depend on judgment instead of a boolean value: whether a document set is too large, whether a message is urgent, or whether the available evidence is sufficient.
I represented those branches as decision cases:
decision {
case: there are too many documents {
Summarize each group before comparing them.
}
case: the documents fit in context {
Compare them directly.
}
}The runtime sends the case descriptions and current context to the model through a constrained tool call. The model selects one case, and the interpreter executes that body.
This differs from an if statement in an important way. The runtime does not evaluate there are too many documents as an expression. The phrase describes a judgment the model must make from the current situation.
The tradeoff is explicit. A parser can verify that the block has cases and valid bodies. It cannot prove that a case is true or that two cases cover every possibility. The model supplies that semantic judgment.
Calls receive context explicitly
Early versions allowed tool calls to see whatever data happened to be present in the session. The same call could behave differently depending on which earlier paragraph had loaded an object.
I made dependencies explicit. A function only receives the objects passed to it or loaded into its current scope.
These forms therefore resolve to the same operation:
@Analyze my @productivity and @sleep from last month.
@load("sleep")
@load("productivity")
@Analyze my productivity and sleep from last month.In the first form, inline mentions provide the dependencies. In the second, prior statements load them into the current scope. The @Analyze call receives the resolved sleep and productivity objects in both cases.
Event blocks create their own scopes:
watch @Telegram.NewMessage {
@load the latest messages from @ConversationHistory
Draft a reply using the new message and recent conversation.
@Telegram.SendMessage the reply.
Update @ConversationHistory.
}Entering the block adds the triggering event to the scope. Each paragraph can read the event and the values produced by earlier paragraphs in that block. Values outside the scope remain unavailable unless the document loads them.
This made context management visible in the document. A reader can see where data entered, which operations used it, and when it left scope.
The AST is the canonical program
Plain text was insufficient once the editor needed to distinguish mentions, calls, decisions, scopes, and prose. I made the AST the stored program and treated the document as its editable view.
The node set stayed small:
section
body
prompt statement
function-call statement
decision
watch statement
variable expression
dynamic expressionEach paragraph becomes a statement node. Inline mentions become references or function calls. Decision and watch blocks own nested bodies. Plain prose remains a prompt statement.
A recursive tree-walking interpreter executes the nodes in document order. That implementation made control flow easy to inspect and produced an execution trace from the same traversal. Each trace entry points back to the node that ran.
The editor also works against the tree. Inserting a mention creates a mention node with an object reference; renaming visible text does not silently change the underlying identity. Moving a paragraph moves one statement node. Structured editing prevents a malformed brace or misspelled directive from becoming a runtime surprise.
Models can edit the same representation through structured operations. They add or replace nodes instead of regenerating the entire prompt as text, which preserves parts of the document the model did not intend to change.
Dynamic expressions resolve against the current scope
Some values cannot be fixed when the document is written. They depend on data produced earlier in the same run. Idyllic uses {...} for a value the model should derive immediately before executing its containing statement.
watch @time(everyday 9am) {
Load the tokens in @CoinsILike.
Check their current prices on @Coinbase.
@Telegram.SendMessage {the five best-performing tokens}.
Update @ConversationHistory.
}When the interpreter reaches the send call, it resolves the dynamic expression against the current scope. At that point the scope contains the token list and price results. The model returns the value required by the call, and the interpreter passes that value to SendMessage.
The same syntax can choose an amount based on context:
Load {enough recent messages to understand the conversation} from @ConversationHistory.The expression describes a value instead of an instruction sequence. Its containing function still defines the operation: load messages. The model only supplies the argument.
This kept dynamic judgment local. The runtime controls which function runs and when. The model fills the value that cannot be determined until execution reaches that point.
Structured and prose statements can coexist
I did not require every paragraph to compile into a fully specified function call. That would force writers to resolve every detail before running a draft.
The AST stores two kinds of executable statements:
- A structured statement names the function, arguments, and dependencies. The interpreter follows that representation directly.
- A prompt statement contains prose. An agent interprets the instruction using the current scope.
Writers can begin with prose and convert important paragraphs into structured calls later. The editor uses an agent to propose that conversion because mapping an instruction to the right tool and arguments requires semantic judgment. Once the writer accepts the structured form, future runs use the same operation.
This makes predictability local. A stable integration call can be fully structured. An exploratory analysis can remain prose. Both live in the same document and share the same scopes and execution trace.
The probabilistic step stays visible. If a prose paragraph is interpreted differently on two runs, the trace shows that it was an agent-interpreted node. A structured call does not quietly fall back to model interpretation.
The language keeps the runtime small
The Idyllic runtime only needs a few responsibilities:
- Walk the AST in document order.
- Maintain explicit context scopes.
- Resolve typed mentions and function calls.
- Ask the model to select decision cases or fill dynamic values.
- Interpret the paragraphs that remain prose.
- Record each node's execution in a trace.
The model handles judgments expressed in natural language. The interpreter handles ordering, scope, calls, and state. The document shows which side owns each decision.
That boundary was the point of the language. I did not need prompts to become deterministic programs. I needed repeated runs to stop making accidental choices about tool identity, context, order, and control flow.
Idyllic adds those choices where the writer wants them and leaves the rest as prose. A prompt can begin as an ordinary document, become more structured as its workflow stabilizes, and remain readable throughout.