Build workflows
Workspaces
Give a run a folder of its own, clone a repository into it, and hand the work to a coding agent — then check the diff, run the tests and open a pull request.
Most tools reach outward: post to Slack, query Postgres, comment on a pull request. Workspaces do the opposite. They give a run a folder of its own, isolated in a Docker sandbox, where it can clone a repository, change files, run your test suite and push a branch.
They are available to every account with nothing to set up, under the workspace. prefix, and they work in tool steps and in an agent's tools list just like built-in tools do. The one difference: they only exist while a run is going. The folder is created when a step asks for it and deleted — with everything in it — the moment the run finishes.
A first workspace
steps:
- key: checkout
kind: tool
tool: workspace.create
args: { repo: northwind/api, branch: main, connection: github }
- key: fix
kind: tool
tool: workspace.agent
args:
prompt: |
test/orders_test.rb fails on the currency rounding case.
Find the cause and fix it. Don't change the test.
- key: tests
kind: tool
tool: workspace.exec
args: { command: "bin/rails test test/orders_test.rb" }
checkout provisions the folder and clones into it, fix hands the job to a coding agent, and tests runs the suite over what it did. Steps in the same run share the workspace, so you only create it once.
The tools
| Tool | Arguments | Returns |
|---|---|---|
workspace.create |
name (default main), repo as owner/name or an https URL, branch, connection, depth (default 1, 0 for full history) |
{ workspace, repo, branch, head } |
workspace.read_file |
workspace, path, max_bytes, or start_line and line_count for part of it |
The file as text, truncated at 100 KB, or { content, start_line, end_line, total_lines } for a range |
workspace.write_file |
workspace, path, content |
{ path, bytes } |
workspace.list_files |
workspace, path, depth |
{ path, kind, bytes } for each entry, ignoring .git |
workspace.find_files |
workspace, pattern (a file-name glob like *_test.rb), path, limit (default 200) |
{ files, truncated } |
workspace.search |
workspace, pattern, path, glob, ignore_case, fixed, limit (default 100) |
{ matches: [{ path, line, text }], truncated } |
workspace.query_file |
workspace, path of a JSON or YAML file, query in JMESPath |
Only the part the query selects |
workspace.detect_project |
workspace, path |
{ stacks, make_targets, ci }: each language with its package manager and likely test, lint and build commands |
workspace.exec |
workspace, command, timeout |
{ exit_code, output } |
workspace.agent |
workspace, prompt, model, continue, timeout |
{ text, session_id, files_changed } |
workspace.diff |
workspace, path, stat, base |
The uncommitted changes as a patch, or everything since the branch left base |
workspace.log |
workspace, ref, path, since, limit (default 20) |
[{ sha, author, date, subject }], newest first |
workspace.show |
workspace, ref (default HEAD), path, stat |
The commit's message and patch, or the file at path as it was at ref |
workspace.blame |
workspace, path, start_line, end_line (at most 100 lines) |
[{ line, text, sha, author, date, summary }] |
workspace.commit |
workspace, message, paths |
{ sha, branch, files } |
workspace.push |
workspace, branch, connection |
{ branch, repo } |
workspace.destroy |
workspace |
{ destroyed } |
search and find_files skip .git, binary files and anything the repository's .gitignore leaves out. They use ripgrep and fd when the sandbox has them, and git grep, git ls-files, grep and find when it doesn't. The patterns are extended regular expressions, or plain text with fixed: true.
None of the reading tools change the workspace, so a tool step that calls one is simply repeated if a restart interrupts it, while exec, agent, write_file, commit and push are not. Give an agent search, read_file with a line range and detect_project to find its way around a repository, rather than exec with grep and cat: it gets structured results and can't run anything by mistake.
name and workspace are only worth passing when a run needs two folders at once — say, one repository to read from and another to write to. A run may have four.
The coding agent
workspace.agent runs a coding agent over the folder. It reads the files itself, edits them, and runs commands in the sandbox, in a loop, until it is done — so give it the job the way you would give it to a developer, not as a list of edits:
The invoice PDF renders the total in cents. Find where it's formatted, fix it, and add a test that would have caught it.
What comes back is text, the agent's own account of what it did, and files_changed, the paths it touched. Neither is proof, so check it: workspace.exec to run the tests, workspace.diff to read the patch, or an llm step to review it before anything is pushed.
continue: truecarries on the previous agent call's session in that workspace, which is how you say "now update the docs for what you just changed" without repeating the context.modelnames one of your AI models. A Murmurator AI model is refused whichever agent you pick, because the agent calls the provider directly and we can't meter what it spends.- Time. A turn is capped at 10 minutes by default;
timeoutlowers or raises it. Long agent turns hold the run open, so prefer several focused calls to one enormous one.
Choosing an agent
workspace.create takes an agent, and the sandbox is built from that agent's image, so the choice is made once per workspace rather than per call.
| agent | models it can use |
|---|---|
opencode (default) |
Anthropic, OpenAI, Gemini, OpenRouter, Mistral, DeepSeek, xAI, and a self-hosted Ollama |
codex |
OpenAI and a self-hosted Ollama. codex speaks only the OpenAI responses API, so the others are refused. |
- key: work
kind: tool
tool: workspace.create
args: { repo: acme/api, connection: github, agent: codex }
Self-hosted models
Both agents can run on a model you host yourself. Murmurator points the agent at your Ollama, keeping the configuration outside the workspace so it can never end up in a commit, and opens the sandbox's network policy to your Ollama host and nothing else — a sandbox blocks outbound traffic otherwise, which is most of the point of it. A model on the same machine as Murmurator is reached at host.docker.internal, which we substitute for localhost for you.
Two things to get right before you rely on it:
- Context window. Each agent sends several thousand tokens of its own instructions ahead of your prompt, far past Ollama's 4,096 default. Raise it with
OLLAMA_CONTEXT_LENGTHor the model'snum_ctx, and pick a model that calls tools well — a small one tends to describe the edit rather than make it. - Time. Those instructions go out again every turn, so an agent call against your own hardware runs for minutes where a hosted model takes seconds. Raise
MURMURATOR_WORKSPACE_AGENT_TIMEOUTwell past its 600 second default.
Git
workspace.create clones over https, and workspace.push pushes back, using the token from the GitHub connection you name in connection. Both obey that connection's allowed repositories, so a workspace can never reach a repository the connection itself can't.
- key: branch
kind: tool
tool: workspace.commit
args: { message: "Fix currency rounding in invoice totals" }
- key: publish
kind: tool
tool: workspace.push
args: { branch: "automated/rounding-{{ run.number }}" }
- key: pr
kind: tool
tool: github.create_pull_request
args:
repo: northwind/api
head: "automated/rounding-{{ run.number }}"
base: main
title: "Fix currency rounding in invoice totals"
body: "{{ steps.fix.output.text }}"
Push to a fresh branch rather than the default one, and let a person merge. The clone is shallow by default; pass depth: 0 if you need the full history. log, blame and diff with a base all need it: a shallow clone has only its latest commit and only the branch it checked out, so compare against a remote branch such as origin/main.
What a workspace can and can't reach
The folder lives inside a Docker sandbox. Everything a run does to it — reading, writing, git, the commands you run and the agent itself — happens in there, not on our servers.
- Paths stay inside the workspace.
.., an absolute path and a symlink pointing out are all refused. - Only the secret a command needs. Your connection credentials stay out of the sandbox, with two deliberate exceptions: a GitHub connection's token goes to the one git command that clones or pushes, and never reaches
.git/config, a command line or the agent's environment; and the coding agent gets the key for the model it calls, because it calls the provider itself. Both arrive in an environment file rather than on a command line, and both are scrubbed out of run logs. A model you host yourself needs no key, so nothing is passed at all. - Nothing else gets out. The sandbox blocks outbound traffic apart from what the agent needs, and a self-hosted model's host is the only address opened for it.
- Nothing survives. When the run ends, the folder and the sandbox go with it. Anything you want to keep has to leave as a step's output, a commit, a pushed branch or a Slack message.
- A failing command is not a failing step.
workspace.exechands backexit_code, so a red test suite is something your workflow can branch on rather than something that stops it.
One thing worth thinking about before you turn this on: a coding agent reads the repository, and repositories are written by other people. A README, an issue template or a comment in a test fixture can try to talk it into something you didn't ask for. The defence is the same as everywhere else in Murmurator — give the connection an allowed repositories list, push to a branch, and keep a person on the merge button.