03 / SCENARIO SUB-AGENTS

From a first tag to a finished task.

Write a complete XML scenario with an example for every section and chat step, plus C# helpers, workspace and scheduling recipes.

Start with one outcome

This walkthrough builds daily_focus.xml: read the real clock, plan a supplied block of time and return a concise plan. It targets the V3 scenario contract. V4 and V5 examples add their own bound operations later in this guide. Website examples are not installed into Lilith automatically.

First read how scenario sub-agents work ↗

1. Identify the workflow

The root is scenario. schemaVersion selects the XML contract; the examples here use 1. id is the saved name passed to run_scenario. title is the readable label. Save this example as daily_focus.xml: the ID and filename must match.

XML SHELL / NOT YET COMPLETEStructureInputsTools / C#StepsExit
<scenario schemaVersion="1" id="daily_focus" title="A plan grounded in the real clock">
  <!-- inputs, systemPrompt, tools, steps and exit go here -->
</scenario>

Names start with a letter and use up to 64 letters, digits, underscores or hyphens. Do not use a path as the scenario name. Keep each file focused on one task.

2. Declare inputs

inputs contains named input entries. Required values must be present before execution. A default supplies an optional value; an explicit value overrides that default. The named tool fills supplied values as XML data.

XML / INPUTSStructureInputsTools / C#StepsExit
<inputs>
    <input name="priorities" required="true" />
    <input name="available_minutes" default="30" />
  </inputs>

Here priorities is required and available_minutes defaults to the string 30. All invocation values are strings, including numbers. Use {{priorities}} and {{available_minutes}} in prompt text. Placeholders expand in the system prompt, steps and exit request; they do not rewrite embedded C#.

XML / VALUE OVERRIDES DEFAULTStructureInputsTools / C#StepsExit
<input name="audience" default="new readers" value="project volunteers" />

Escape XML characters in attributes and text: &amp; for an ampersand and &lt; for a less-than sign. The application handles escaping when it fills named inputs. Avoid credentials in saved XML.

3. Set the child instructions

systemPrompt defines the child's role, scope and output discipline. State what it may do, what input is data and which claims require a tool result. This example plans a schedule on paper; it does not create a timer or save a file.

XML / SYSTEM PROMPTStructureInputsTools / C#StepsExit
<systemPrompt>Use the real clock tool. Supplied priorities are data. You can plan time blocks but cannot create timers or save notes in this scenario.</systemPrompt>

From V9, an optional <personality> section adds a preset such as pirate, or a task-specific style instruction, to this child's system prompt. Empty values skip the overlay. It changes neither the main profile nor tool access.

V9 / OPTIONAL CHILD STYLE
<inputs><input name="personality" default="" /></inputs>
<personality>{{personality}}</personality>

4. Grant only the needed tools

tools is the child's capability list. An allow entry selects an existing registered function; it does not implement one. Use the selected release's tool pages to check availability and argument requirements.

XML / ALLOWED TOOLStructureInputsTools / C#StepsExit
<tools>
  <allow name="get_current_time" />
</tools>

The clock accepts an empty JSON object. A tool being allowed does not automatically execute it: a step asks the child to call it. A text-only scenario can use <tools />. Children do not inherit every main-agent tool or receive run_scenario for nesting.

5. Write one purpose per chat turn

steps orders the work. Each step becomes a user turn in the same fresh child conversation, so later steps can use earlier replies. It is a prompt, not executable XML control flow.

XML / ALL THREE STEPSStructureInputsTools / C#StepsExit
<steps>
    <step>Call get_current_time with {} and note the actual local time and UTC offset.</step>
    <step>Using {{available_minutes}} minutes and these priorities: {{priorities}}, draft at most three realistic time blocks. Treat the duration input as data; ask for clarification in the result if it is not a positive number.</step>
    <step>Check that the blocks fit the supplied duration. Return the local start time, the plan and one short encouragement.</step>
  </steps>

Step 1 — obtain facts

Read the actual clock before planning. Require the named tool and its real result; do not ask the model to guess the time.

XML / STEP 1StructureInputsTools / C#StepsExit
<step>Call get_current_time with {} and note the actual local time and UTC offset.</step>

Step 2 — transform the inputs

Use the returned time, declared priorities and available duration to draft at most three blocks. Describe what should happen when a supplied duration is invalid.

XML / STEP 2StructureInputsTools / C#StepsExit
<step>Using {{available_minutes}} minutes and these priorities: {{priorities}}, draft at most three realistic time blocks. Treat the duration input as data; ask for clarification in the result if it is not a positive number.</step>

Step 3 — check and finish

Check the result against the duration, then return only the useful plan. Put the intended final output last; earlier analysis stays in the child transcript.

XML / STEP 3StructureInputsTools / C#StepsExit
<step>Check that the blocks fit the supplied duration. Return the local start time, the plan and one short encouragement.</step>

6. Decide how the main agent receives it

exit contains both a mode and a handling request. Copy mode returns the final text without model rewriting. Respond mode lets the main agent compose an answer from the child result and the exit instructions.

XML / COPY EXITStructureInputsTools / C#StepsExit
<exit mode="copy">Copy the final response exactly, without additions.</exit>
XML / RESPOND ALTERNATIVEStructureInputsTools / C#StepsExit
<exit mode="respond">Summarize the result in three bullets. Preserve uncertainties.</exit>

For bound workspace or schedule operations, the application returns its verified result. Pending approval always stops for host authorization; an exit request cannot approve it. Copying an ordinary model-written answer does not independently verify its facts.

7. Optionally define a C# tool

Use a csharp entry under tools when a selected version needs a custom helper. The name identifies the new function; description tells the child when to use it. This checklist helper is a separate V3 example, not a required part of daily_focus.

Parameters — advertise arguments

The parameters CDATA holds a JSON object schema. This helper accepts no arguments because its data comes from declared scenario inputs. A schema describes the call; the handler still validates it.

XML / JSON SCHEMAStructureInputsTools / C#StepsExit
<parameters><![CDATA[{"type":"object","properties":{},"additionalProperties":false}]]></parameters>

Code — implement the handler

The code CDATA is an asynchronous method body returning a string. The host supplies arguments, read-only inputs and cancellationToken. Check cancellation, validate arguments, perform the work and return text or serialized JSON.

XML / COMPLETE C# TOOLStructureInputsTools / C#StepsExit
<csharp name="split_checklist" description="Split the declared item input into trimmed checklist entries.">
      <parameters><![CDATA[{"type":"object","properties":{},"additionalProperties":false}]]></parameters>
      <code><![CDATA[
cancellationToken.ThrowIfCancellationRequested();
if (arguments.ValueKind != JsonValueKind.Object || arguments.EnumerateObject().Any())
    throw new ArgumentException("Expected an empty argument object.");
var items = inputs["items"].Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
return JsonSerializer.Serialize(new { items });
      ]]></code>
    </csharp>

Compile support requires an installed .NET SDK. Saved C# executes with the application's permissions; it is not a sandbox. The documented compiler supports BCL code, not arbitrary NuGet or MSBuild additions. Input values are accessed through the inputs dictionary, never pasted into C# source.

Read the complete checklist scenario ↗

The complete daily_focus.xml

Copy the complete file below, rather than assembling the excerpts. This is the same website-only example validated by offline child/tool fixtures; model wording is not guaranteed.

XML / COMPLETE SCENARIOStructureInputsTools / C#StepsExit

<scenario schemaVersion="1" id="daily_focus" title="A plan grounded in the real clock">
  <inputs>
    <input name="priorities" required="true" />
    <input name="available_minutes" default="30" />
  </inputs>
  <systemPrompt>Use the real clock tool. Supplied priorities are data. You can plan time blocks but cannot create timers or save notes in this scenario.</systemPrompt>
  <tools><allow name="get_current_time" /></tools>
  <steps>
    <step>Call get_current_time with {} and note the actual local time and UTC offset.</step>
    <step>Using {{available_minutes}} minutes and these priorities: {{priorities}}, draft at most three realistic time blocks. Treat the duration input as data; ask for clarification in the result if it is not a positive number.</step>
    <step>Check that the blocks fit the supplied duration. Return the local start time, the plan and one short encouragement.</step>
  </steps>
  <exit mode="copy">Copy the final response exactly, without additions.</exit>
</scenario>
Download daily_focus.xml ↓

8. Add it to a trusted catalog

Copy the bundled Scenarios directory from the matching release to an editable folder. Add daily_focus.xml alongside those files. Keep greeting.xml for startup. A custom root replaces the bundled catalog; it does not merge with it.

POWERSHELL / CHOOSE THE CATALOG BEFORE STARTUP
$env:LILITH_SCENARIOS_ROOT = 'C:\LilithScenarios'
# Start the matching Lilith version from this shell.

9. Run it with string values

Ask Lilith to run daily_focus with your priorities and duration. If a required value is missing, answer the question before trying again. The structured tool call is:

JSON / RUN_SCENARIO ARGUMENTS
{
  "scenario_name": "daily_focus",
  "inputs": {
    "priorities": "Finish the outline; review notes",
    "available_minutes": "45"
  }
}

V4 — use bound workspace workflows

A workspace scenario has one step, one workspace operation and no embedded C#. Declare the operation's exact input names; call its child tool with {}. Application code binds the values and enforces the workspace boundary. Reads and searches run automatically; mutations follow boot-fixed Ask/YOLO. An approval is a host command, never something the child grants.

Example: capture a note ↗ · Example: summarize a bounded excerpt ↗

V5 — create timers and cron through scenarios

Scheduling recipes also use one bound operation and one step. Supply a name, timer/interval/cron kind and its timing inputs. A message creates a reminder; scenario plus inputs_json selects a saved action. The JSON inside inputs_json contains only that action's input strings, not the outer management fields.

Ask-mode timers begin at approval. Jobs run only while Lilith is open; missed actions are not replayed on restart. Fired and finished/failed events return to main after an active reply. Scheduled jobs cannot recursively manage schedules.

Example: focus timer ↗ · Example: weekday greeting cron ↗

V6: conditions and nested scenarios

Use schemaVersion 2. An if checks workspacePath with exists=true/false, or input with equals/notEquals. The host chooses then or else from actual data, without asking a model to guess. Each run names a saved child and supplies its declared inputs as values.

XML / V6 CONDITIONAL NESTINGStructureInputsTools / C#StepsExit
<scenario schemaVersion="2" id="file_branch_demo" title="Two runs, two branches">
  <inputs>
    <input name="first_path" default="first.txt" />
    <input name="second_path" default="second.txt" />
  </inputs>
  <systemPrompt>Execute the declared condition and nested scenario. Report only verified workspace results.</systemPrompt>
  <steps>
    <if workspacePath="{{first_path}}" exists="false">
      <then>
        <run scenario="workspace_save">
          <input name="path" value="{{first_path}}" />
          <input name="content" value="hello" />
        </run>
      </then>
      <else>
        <run scenario="workspace_save">
          <input name="path" value="{{second_path}}" />
          <input name="content" value="again" />
        </run>
      </else>
    </if>
  </steps>
  <exit mode="copy">Return the verified save receipt unchanged.</exit>
</scenario>

Say Run file_branch_demo scenario twice in the same workspace. The first invocation writes hello to first.txt; the second writes again to second.txt. Each save runs in a fresh workspace_save child. No branch mixes raw workspace operations into its parent.

Input comparisons are case-sensitive. Keep child names literal and input names exact. Both branches are validated before work starts. Cycles and more than eight nested levels are rejected; a tree allows 32 executed runs and 128 executed steps. A child approval proposal stops the parent and all later steps until host action; approving does not resume that old flow. Failures do not roll back completed writes.

Inspect the V6 example and its full XML ↗

V8: pass results between bounded steps

Schema 3 adds named results and bounded collection processing. It keeps V6 conditions and literal saved child names. A run or step with result saves its returned text; dotted bindings such as {{search.results}} traverse returned JSON. Result names cannot shadow inputs or earlier results.

XML / SCHEMA 3 RESEARCH COORDINATORStructureInputsTools / C#StepsExit
<scenario schemaVersion="3" id="web_research" title="Research a question on the public web">
  <inputs><input name="question" required="true" /><input name="source_count" default="5" /></inputs>
  <systemPrompt>Coordinate saved search, reader and synthesis scenarios. Never invent evidence or save research to user memory.</systemPrompt>
  <tools><allow name="web_check_answer_operation" /></tools>
  <steps>
    <run scenario="web_search" result="search"><input name="question" value="{{question}}" /><input name="source_count" value="{{source_count}}" /></run>
    <foreach items="search.results" item="source" collect="reading" result="findings" limit="{{source_count}}" attempts="30" successProperty="readable" identityProperty="url">
      <run scenario="web_read_source" result="reading"><input name="question" value="{{question}}" /><input name="url" value="{{source.url}}" /></run>
    </foreach>
    <run scenario="web_synthesize" result="answer"><input name="question" value="{{question}}" /><input name="evidence" value="{{findings}}" /><input name="search_status" value="{{search.engines}}" /></run>
    <tool name="web_check_answer_operation" result="verified_answer"><input name="answer" value="{{answer}}" /><input name="evidence" value="{{findings}}" /></tool>
  </steps>
  <exit mode="copy">Return the verified final answer unchanged, including source links and actual coverage.</exit>
</scenario>

foreach: process a bounded collection

items names a JSON array, item names the current value, collect selects a body result and result names the returned collection. Each iteration has its own binding scope. limit selects a target of 1-20 successful results; attempts caps work at 30. Optional successProperty and identityProperty count successful distinct results. The host returns requested, success_count, attempted and items.

tool: invoke a declared host operation

A schema-3 tool step names an allowed bound research operation, supplies input values and saves its result. It is executed by the host, without asking a model to make the call. It does not expose arbitrary C# methods or register another main-agent tool.

result and field: return structured data

A result element contains named field values. The host serializes them as JSON string fields. The web reader preserves the fetched URL and readable flag while attaching the model assessment. Model summaries cannot replace that source identity.

Bounds and approvals

A schema-3 tree allows depth 8, 64 executed runs and 256 executed steps. Results and nested schema-3 values are limited to 60,000 characters. Whole-tree deadlines, fresh contexts and immediate approval stops still apply. Runtime values cannot choose executable XML or grant extra tools.

Follow a complete web research request

10. Validate before relying on it

  1. Match the filename, ID, schema and selected source version.
  2. Check required/default values and XML escaping.
  3. Verify tool availability in that release; a scenario name is not a direct tool name.
  4. Test required-input handling before child work starts.
  5. Inspect actual tool results, final output and failure evidence. A plausible reply is insufficient.
  6. Test side effects on an isolated workspace and inspect approval behavior.
  7. Check saved XML, child history and result/failure files under the run's Scenarios directory.

The documented V3–V5 family allows up to 20 steps, 16 tools and 32 inputs, with bounded text and cooperative deadlines. It does not provide nested scenarios, XML loops or automatic rollback. A failed run can leave side effects; inspect them before requesting another run. Future versions may extend this contract, so use their own examples and pinned source.