Tutorials
Run a background agent
Build a daemon-hosted workflow with timers and background events.
View Markdown source ↗Schedule a bounded timer and observe the host stop the session after the client disconnects.
Prerequisites and command context
Section titled “Prerequisites and command context”Complete the Unix daemon tutorial and use a separate private demo directory. Run the checkout commands from the repository root with its active opam environment. This first example uses a detached daemon host and requires no provider key when you send no model messages. ChatML and scheduling are host-dependent; native local exit would stop its process.
Read the current provider TLS and permission boundaries before model work or deployment. Build troubleshooting includes the Apple Silicon/OpenBLAS setup path.
This first example uses a timer, not a paid model request. Prepare the
private tutorial directory.
Edit the private unix.sexp prompt entry’s path to the absolute path of the
tracked timer.chatmd.
Keep its configured ID hello and workspace project; only the source path changes.
Validate and start the daemon as in the Unix tutorial. In another terminal create a detached session:
dune exec bin/chat_tui.exe -- --no-config --connect "unix://$OCHAT_DEMO/agent.sock" \ --new-daemon-session --prompt hello --workspace project --detachedThe moderator schedules Tick after ten seconds at session start. Quit the TUI
before ten seconds elapse. The daemon, not the client, delivers the event; the
handler requests session end with scheduled tutorial stop. List/reconnect after
the timer fires and inspect session/schedule state. Do not submit a user message:
that would add an unrelated model turn to an intentionally offline example.
With the stdio client, schedule.list takes the returned session_id and positive
limit; session.get shows session state. Initialize a new connection before
reattachment. If using a TUI to inspect a stopped session, attachment is not a
request to restart its script.
Extend to asynchronous model work
Section titled “Extend to asynchronous model work”The orchestration guide describes Model.spawn,
completion events, budgets, jobs and cancellation. Use the complete tested
background recipe example
as an embedding/script reference: agent_prompt_v1 takes a JSON object containing
the prompt, input and is_local, and completion handlers dispatch by job ID.
Replace fake-provider setup only after choosing real credentials/model/budget.
For an unattended production script, configure narrow tools, explicit permission
fallback, job limits and misfire policy. Keep detached liveness if no client must
own it. Check job.list/get and schedule.list/get after reconnect; cancel with a
writable authorized attachment. Stop the daemon normally when done. On restart,
durable scheduling intent and delivery records recover, but in-flight external
effects are not automatically safe to rerun.
Checkpoint, troubleshooting, and next step
Section titled “Checkpoint, troubleshooting, and next step”After the timer fires, session.get should report the ended session and schedule.list should show the delivered timer. A disconnected client alone is not evidence of background execution. If nothing happens, check that the private prompt path names timer.chatmd, the daemon stayed alive, and the session was created after the configuration change. The example schedules at session start; reconnecting is not a request to restart the script. Shut down clients and daemon before archiving/removing the recorded private directory; durable state persists there until then. Next, inspect scheduling through the stdio client or study orchestration.
View source · Background timer2 files
Complete source files. Open a filename to read it here.
timer.chatmdStart here
<developer>This host demonstrates a background timer; no model request is needed.</developer>
<script language="chatml" kind="moderator">
type state = int
type event = [ `Session_start | `Tick ]
let initial_state = 0
let on_event : context -> state -> event -> state task =
fun ctx st ev ->
match ev with
| `Session_start ->
Task.bind(Schedule.after_ms(10000, `Tick), fun timer_id ->
Task.pure(st + 1))
| `Tick ->
Task.bind(Runtime.end_session("scheduled tutorial stop"), fun ignored_end ->
Task.pure(st + 1))
</script>
LICENSE.txtnotice
MIT License
Copyright (c) 2025
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.