View source on GitHub
POST /api/v1/app-conversations accepts a system_prompt field. When set,
it replaces the default static system prompt for that conversation. The
per-conversation dynamic context — repo context, loaded skills, custom
secrets, current datetime — is still appended automatically, so your custom
prompt composes with whatever runtime context the sandbox provides.
Feature shipped in OpenHands Enterprise 1.62.0
(enterprise#335) and is
also live on OpenHands Cloud.
When to Use This
- Your agent has a specialised role (research assistant, code reviewer, documentation writer) and the default coding-agent framing gets in the way.
- You’re building a domain-specific product on top of OpenHands and want to own the prompt without asking the agent to ignore the default one.
- You need to enforce specific behaviour or persona for a single conversation without changing user- or org-level settings.
system_prompt is a per-conversation override.
What Gets Replaced, and What Doesn’t
SystemPromptEvent is the first event in every conversation and carries
two structured fields:
system_message_suffix is a separate field on the same request and is
appended to the dynamic context; you can set both.
Anatomy of the Default System Prompt
Before you replace the default prompt wholesale, it’s worth knowing what you’re displacing. The SDK composes the prompt from a small set of named, guarded sections. The references below are pinned tosoftware-agent-sdk@v1.50.1
so the line numbers and content stay stable; bump the tag when auditing a
newer release.
Two blocks on one system message
The default agent ships the prompt as aSystemPromptEvent with two
content blocks on a single role: system message
(event/llm_convertible/system.py):
The caching split is wired in
llm/llm.py::_apply_prompt_caching
— for Anthropic-style prefix caching, only index 0 gets the cache marker.
Every conversation your account starts can hit the same cached static
block, which is the whole reason the two-block shape exists.
What goes into each block
Static-tier sections live incontext/prompts/sections/static.py;
dynamic-tier sections in
context/prompts/sections/dynamic.py.
The default composition order is pinned in
context/prompts/presets.py:
Static block, in order — <SOUL> and <ROLE> (identity), <MEMORY>
(AGENTS.md or persistent-memory guidance), <EFFICIENCY>,
<FILE_SYSTEM_GUIDELINES>, <CODE_QUALITY>, <VERSION_CONTROL>,
<PULL_REQUESTS>, <PROBLEM_SOLVING_WORKFLOW>, <SELF_DOCUMENTATION>
(work habits), <SECURITY> and <SECURITY_RISK_ASSESSMENT> (safety
policy), <BROWSER_TOOLS> (only if enable_browser),
<EXTERNAL_SERVICES>, <ENVIRONMENT_SETUP>, <TROUBLESHOOTING>,
<PROCESS_MANAGEMENT>, and <IMPORTANT> (per-model-family tweaks for
Claude, Gemini, GPT-5).
Dynamic block, in order — <REPO_CONTEXT>, <MEMORY_CONTEXT>,
<SKILLS>, your system_message_suffix (raw, no wrapper),
<CUSTOM_SECRETS>, and <CURRENT_DATETIME> last on purpose: it’s
the only per-conversation volatile value, so putting it at the tail keeps
the stable dynamic content a cache-friendly prefix even on providers that
cache the dynamic block too.
Setting system_prompt on POST /api/v1/app-conversations replaces the
entire static block above — all 17-ish sections, verbatim text and all —
with your custom text. The dynamic block is unaffected.
Writing a custom system_prompt without breaking caching
The static block’s whole value is that it’s the same bytes every time your
account hits the LLM. Anthropic’s prefix cache keys on the exact byte
prefix: a one-character change splits one cached prefix into two, each
with its own cold-start cost on the next miss. OpenAI’s and Gemini’s
caches have the same shape of pitfall. So when you write your own
system_prompt:
- Don’t interpolate per-conversation volatile values into the text.
No
datetime.now(), no request id, no conversation id, no user email, no repo URL, no sandbox id, no working directory. Any of these in the static block means every conversation gets its own cache entry — effectively no caching at all. - Don’t interpolate per-user profile fields either (API key, display name, org name). Even if the value changes rarely, it still shards the cache per-user, and anything secret-shaped doesn’t belong in a cached blob.
- If you need date-aware behaviour, read the dynamic block. The server
always appends
<CURRENT_DATETIME>todynamic_context— reference it from the static text (e.g. “see<CURRENT_DATETIME>for today’s date”) instead of baking a timestamp in. - Keep the exact same bytes across runs that are logically “the same agent”. If you tweak wording, do it on a deploy boundary, not inside the request path.
- Front-load the parts you’re least likely to edit. On a tail-only edit, the longest unchanged prefix still hits the cache; on a prefix edit, nothing does.
- If you need per-conversation flavour, use
system_message_suffix(goes into the uncached dynamic block) rather than templating it intosystem_prompt.
hashlib.sha256(system_prompt.encode()).hexdigest()[:12]) and log it. If
that hash changes between two conversations that should be equivalent, you
have a cache leak.
system_prompt vs. skills vs. system_message_suffix — when to use which
All three let you shape the agent’s instructions, but they pay very
different context-window and caching costs:
Rule of thumb — if the instruction applies to every turn of every
conversation this agent runs, it belongs in
system_prompt. If it
applies sometimes, make it a skill. If it’s a one-conversation tweak,
use system_message_suffix.
Resist the urge to pack long domain procedures into system_prompt just
because it feels tidier. Every token in the static block is a token the
LLM reads before responding to anything — a skill that fires on 1-in-20
turns pays its full-body cost ~5% as often as the static block does.
How It Works
1. Start the conversation
POST /api/v1/app-conversations is asynchronous: it returns a
AppConversationStartTask whose id is the
task id, not the conversation id. Omit sandbox_id and the App Server
provisions a fresh sandbox for you.
2. Wait for app_conversation_id
Poll /api/v1/app-conversations/start-tasks?ids=<task_id> until the task
reaches READY and hands back app_conversation_id (and, if the server
provisioned one, sandbox_id). The endpoint is a “search by ids”
lookup — ids is a required, repeatable query parameter and the
response is a JSON array of the matching tasks.
3. Verify from the SystemPromptEvent
The Cloud events endpoint mirrors the agent-server view, so you can read
the event without needing the sandbox’s session_api_key.
event["system_prompt"] is a {cache_prompt, type, text}
object — the actual prompt string is at .text.
4. Cleanup
DELETE /api/v1/app-conversations/{id} frees the conversation. Delete the
sandbox separately when you’re done with it. The sandbox delete endpoint
is DELETE /api/v1/sandboxes/{id} and additionally requires sandbox_id
as a query parameter — pass the same id in both places, or the server
returns HTTP 422.
Prerequisites
Run It
system_prompt set, fetches the first
SystemPromptEvent, asserts the custom text is there, and cleans up.
Exit status is non-zero if verification fails.
Real output
Captured against an OpenHands Enterprise 1.67.0 instance:Prompt Gallery
Swap--prompt (or SYSTEM_PROMPT) for any of these to try them out.
Research assistant
Code reviewer
Documentation writer
Data analyst
Composition with system_message_suffix
system_message_suffix is a separate field appended to the dynamic
context. You can use both together:
Planning Agent (agent_type=plan)
When agent_type=plan is set alongside system_prompt, the custom prompt
still replaces the built-in planning static prompt, but the planning tools
and workflow instructions are kept — the agent keeps its ability to create
and update plans, with your text as the foundation.
ACP Agents
For ACP (Agent Communication Protocol) agents, which delegate to external CLIs (Claude Code, Gemini CLI, etc.),system_prompt is ignored with a
server-side warning (app_conversation_start:system_prompt_ignored_for_acp_agent).
ACP agents own their own system prompt and cannot be overridden via the REST
API.
Feature Availability
- OpenHands Cloud (currently deployed)
- OpenHands Enterprise 1.62.0+ (enterprise#335)
system_prompt in the
AppConversationStartRequest schema:
APIs Used
All calls use
Authorization: Bearer <OH_API_KEY>.
Related
conversation-tags
same App Conversation API pattern, showing how to attach and read back arbitrary metadata.
load-plugin
start a conversation with a plugin pre-loaded, using the same start-task polling flow.
custom-agent-no-browser
configure which tools the agent has access to (different customisation axis).

