{"slug": "open-webui-creating-custom-agents", "title": "Open WebUI: Creating Custom Agents", "summary": "A developer detailed a step-by-step build of a custom coding agent in Open WebUI v0.9.6, using a custom model configured with a senior Python engineer system prompt, tools, and skills on top of the gpt-5.4-nano base model. The setup connects Open WebUI's Terminal feature to a Docker container for arbitrary code execution and supports chats that update a codebase via the default chat interface and deferred cron jobs.", "body_md": "Open WebUI evolved from an Ollama chat interface into a full-fledged agent interface for any OpenAI API endpoint. Chat context can come from custom knowledge bases for RAG retrieval, text, images, or other chat references, as well as memory. Custom models are built on top of a base model and then configured with a custom prompt, tools, and skills. Finally, the newly added Terminal feature connects to a Docker container to run arbitrary code. With this feature set, Open WebUI promises full agent capabilities.\n\nThis article focuses on the step-by-step creation of a coding agent. You will learn which feature configurations in Open WebUI are relevant, how to set up the agent with its prompts and tools, and finally how to execute chats that result in updates to a codebase via the default chat interface and deferred cron jobs.\n\n*The technical context of this article is `Open WebUI v0.9.6`, published on 2026-06-01. The setup and configuration examples should also work with newer versions.*\n\n*While I am fascinated by the capabilities of artificial intelligence tools and applications, crafting blog articles remains a personal skill. Every character, number, and symbol in this article was typed manually, with the exception of verbatim copies from log messages and screenshots.*\n\n*This article originally appeared at my blog [admantium.com](https://admantium.com/blog/llm41_open_webui_how_to_define_agents/)*.\n\nThe custom coding agent should meet the following requirements:\n\nThe agent is implemented as a custom model, accessible from the Open WebUI GUI via `Workspace`, then `Models`.\n\nFor the base model, I wanted to stay on the conservative side for this test. Specifically, I did not want to be surprised by Codex-class billing for this setup. At the time of writing, `gpt-5.4-nano` offered a good input and output token cost profile. All prices for OpenAI models can be derived from [OpenAI API Pricing](https://developers.openai.com/api/docs/pricing).\n\nFor the system prompt, I used another LLM provider and a creative prompt to produce a non-standard description for the senior Python developer. Here it is:\n\n```\n# Senior Python Engineer — Prompt\n\nYou are a **SENIOR PYTHON ENGINEER** with **FULL-STACK** capability.\n\nYour style combines the enigmatic clarity of a systems thinker, the discipline of a machinist, the memory of an archivist, and the rigor of empiricism-driven validation.\n\n## Role\n\nDesign, build, debug, and improve production-grade software across backends, APIs, data flows, infrastructure boundaries, and frontend integration points.\n\n## Expectations\n\n- Write clean, idiomatic, maintainable Python.\n- Prefer strong typing, clear abstractions, and explicit trade-offs.\n- Think in systems: performance, reliability, observability, security, and developer ergonomics.\n- Operate as a full-stack engineer when needed: backend first, but comfortable with frontend architecture, API contracts, and integration details.\n- Default to pragmatic solutions over fashionable complexity.\n\n## Working Style\n\n- **Enigmatic**: find non-obvious but explainable solutions.\n- **Machinist**: engineer with precision, repeatability, and mechanical discipline.\n- **Archivist**: preserve context, document decisions, and keep codebases legible over time.\n- **Empirical**: validate assumptions through measurement, tests, benchmarks, and evidence.\n\n## Technical Principles\n\n- Use Python thoughtfully: typing, async when justified, testing, profiling, and clear module boundaries.\n- Design APIs and services for scale, debuggability, and failure tolerance.\n- Make observability native: logs, metrics, traces, and actionable errors.\n- Treat security as a design constraint, not an afterthought.\n- Keep code easy to review, extend, and operate.\n\n## Response Rules\n\n- Give concise, technically sharp answers.\n- Provide working code when implementation is requested.\n- State assumptions before designing complex solutions.\n- Explain trade-offs briefly and clearly.\n- Avoid filler, vague advice, and unnecessary abstraction.\n- Favor production-suitable patterns over tutorial-style shortcuts.\n\n## Output Preference\n\nReturn answers as a senior engineer would: precise, grounded, structured, and directly useful.\n```\n\nTo test agent invocation and gauge performance, I decided against adding additional configurations here.\n\nOpen WebUI's default configuration has memory disabled. Each user needs to activate it explicitly. Click on the user icon, then on `Settings`, and then on `Personalization`. Flip the toggle. From then on, each interaction may store persistent facts encountered during use. This section also includes a button to manage memory, for example to list and edit entries.\n\nCapabilities define allowed tool calls which, when enabled, result in an extended system prompt or additional conversation metadata. The default features are common, and the built-in tools are optional tools that can be enabled. If they are enabled, the model can issue tool calls, and Open WebUI will execute them.\n\nThe configuration for the custom model is as follows:\n\nFor most tools, the Open WebUI Docker image is bundled with the necessary libraries, so the container can run code interpretation and provide access to tasks, notes, and memory. Web search and terminal access still need to be configured.\n\nWeb search, as also explained in my [previous article](https://admantium.com/blog/llm39_open_webui_agent_features/), requires you to create an account with a search provider, get an API key, and paste it here.\n\nThe Terminal feature and its setup are a bit more involved and warrant an additional section.\n\nThe [open-terminal GitHub project](https://github.com/open-webui/open-terminal) aims to provide a Python package that exposes a REST API for filesystem interaction and command execution. This API is intended to act as a terminal, providing raw filesystem access.\n\nDocker images with different capabilities are provided in four variants:\n\n`latest`: A heavyweight 4 GB image with a complete C++, Python, Node.js, and data science library stack, and the default user can run `sudo` to install additional packages`slim`: A production image with only `git` as an additional tool`alpine`: A slim variant based on Alpine, further reducing image size to 230 MB`openshift`: A security-context-constrained (SCC) image that further minimizes access rights\nSince I already run Open WebUI as a Docker container, configured via a `docker-compose.yml` file, it is only natural to extend it with an `open-terminal` section. However, I also wanted to customize the image to include the required libraries.\n\nFrom the GitHub repository, we only need `Dockerfile.slim`. Download the repository with this command:\n\n```\ngit clone --depth=1 https://github.com/open-webui/open-terminal.git\n```\n\nThen open the Dockerfile and add all additional libraries that are required. For my goal as a Python engineer, `pip` is required and should be added to the Dockerfile. There is a section for adding custom packages; extend it as follows:\n\n```\n...\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n        python3-pip \\\n        tini \\\n        ...\n```\n\nBuild the image and provide a custom tag.\n\n```\ndocker build -f Dockerfile.slim -t open-terminal-custom-python:20260628 .\n```\n\nAdd this to the compose file from my [Open WebUI setup article](https://admantium.com/blog/llm37_open_webui_setup/).\n\n```\n  open-webui-terminal:\n    image: open-terminal-custom-python:20260628\n    container_name: open-webui-terminal\n    restart: unless-stopped\n    env_file:\n      - ./open-terminal.env\n    volumes:\n      - open-terminal:/home/user\n    networks:\n      - Open WebUI\n```\n\nIn the `open-terminal.env` file, enter the following value with a self-generated secret:\n\n```\nOPEN_TERMINAL_API_KEY=REDACTED\n```\n\nThen start the updated stack with `docker compose up`. The container logs should show the following:\n\n```\n2026-06-28 17:09:37    ____                    _____                   _             _\n2026-06-28 17:09:37   / __ \\                  |_   _|                 (_)           | |\n2026-06-28 17:09:37  | |  | |_ __   ___ _ __   | | ___ _ __ _ __ ___  _ _ __   __ _| |\n2026-06-28 17:09:37  | |  | | '_ \\ / _ | '_ \\  | |/ _ | '__| '_ ` _ \\| | '_ \\ / _` | |\n2026-06-28 17:09:37  | |__| | |_) |  __| | | | | |  __| |  | | | | | | | | | | (_| | |\n2026-06-28 17:09:37   \\____/| .__/ \\___|_| |_| \\_/\\___|_|  |_| |_| |_|_|_| |_|\\__,_|_|\n2026-06-28 17:09:37         | |\n2026-06-28 17:09:37         |_|\n2026-06-28 17:09:37\n2026-06-28 17:09:37   Local:    http://localhost:8000\n2026-06-28 17:09:37   Network:  http://172.18.0.3:8000\n2026-06-28 17:09:37\n2026-06-28 17:09:37   Warning: Listening on all network interfaces.\n2026-06-28 17:09:37   Use --host 127.0.0.1 to restrict to this machine.\n2026-06-28 17:09:37\n2026-06-28 17:09:37   ┌─────────────────────────────────────────────────────────────┐\n2026-06-28 17:09:37   │  ⚠  CORS is set to '*' (allow all origins)                 │\n2026-06-28 17:09:37   │                                                             │\n2026-06-28 17:09:37   │  Any website can make requests to this server.              │\n2026-06-28 17:09:37   │  For production, restrict with:                             │\n2026-06-28 17:09:37   │    --cors-allowed-origins https://your-domain.com           │\n2026-06-28 17:09:37   └─────────────────────────────────────────────────────────────┘\n2026-06-28 17:09:37\n2026-06-28 17:09:38 INFO:     Started server process[3]\n2026-06-28 17:09:38 INFO:     Waiting for application startup.\n2026-06-28 17:09:38 INFO:     Application startup complete.\n2026-06-28 17:09:38 INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)\n```\n\nFinally, the Open WebUI instance needs to be configured for access.\n\n`Admin Panel`\n`Integrations`, then on the `+` symbol next to `Open Terminal`\n`http://open-webui-terminal:8000` for a Docker-internal connection, and enter the API key that you generated above`Save` button\nThe `open-terminal` Docker container logs should show the following message:\n\n```\n2026-06-28 17:37:15 INFO:     172.18.0.2:55262 - \"GET /api/config HTTP/1.1\" 200 OK\n```\n\nIn the first test, the agent should create a custom Python project that implements a REST API. I prepared an initial prompt and follow-ups, but the agent was eager to do everything in a few turns.\n\nInput prompt:\n\n```\nThe repository should use Poetry for dependency management. It should be a GitHub repository, and the agent should also gain access to the GitHub Actions runner pipeline.\n```\n\nTo set this up, the following prompts will be executed one after another.\n\n```\nDefine a GitHub project from scratch for a tiny Python agent experiment. Create a repository named python-rest-api with a short README, .gitignore, app/main.py and app/config.json.\n```\n\nFrom that first prompt alone, the LLM suggested the entire application stack. Here is an excerpt.\n\n```\n  Below is a minimal “from scratch” GitHub project structure you can create locally and then push to GitHub.\n\n  1) Repository root structure\n\n  python-rest-api/\n    README.md\n    .gitignore\n    app/\n      main.py\n      config.json\n\n  2) Create the files\n\n  README.md\n\n  # python-rest-api\n\n  A tiny Python REST API experiment for a minimal agent workflow.\n\n  ## Run\n\n   python -m venv .venv\n  source .venv/bin/activate  # (Windows: .venv\\Scripts\\activate)\n  pip install -r requirements.txt  # if you add dependencies later\n  python app/main.py\n  Endpoints\n  GET /health - health check\n\n  app/main.py\n\n  from __future__ import annotations\n\n  import json\n  import os\n  from http.server import BaseHTTPRequestHandler, HTTPServer\n  from pathlib import Path\n  from typing import Any\n\n  def load_config(config_path: Path) -> dict[str, Any]:\n      with config_path.open(\"r\", encoding=\"utf-8\") as f:\n          return json.load(f)\n\n  /...\n```\n\nWhen asked whether those commands were actually executed, the model answered:\n\n```\nNo—I'm describing what to create, but I didn’t actually run your terminal or generate the repo in your filesystem. I can’t directly execute commands on your machine unless you provide an environment/session where I can run them.\n\nWhat I did instead: I drafted the exact file contents and structure you can create locally, then commit/push.\n```\n\nI learned that the terminal is not enabled by default. In the chat window, there is a small cloud icon that needs to be enabled.\n\nAlso, on the upper-right side, there is a controls section, and its middle files area shows the content of the terminal container. After a few more prompts, and after repeated clarification that the model could execute commands, it produced files.\n\nHowever, it was still difficult to see the output of executed commands. The model repeatedly indicated that the user should run them, or that the environment was not accessible.\n\nThe GUI showed:\n\nExample message:\n\n```\nBecause in this chat I don’t have access to your running process/network interface to observe the results of the command you executed on your side (the earlier assistant message claimed a run/start, but I can’t reliably capture or retrieve the actual response bytes from your local curl).\n\nIn other words:\n\nI can suggest the command and interpret results if you paste them.\nI can’t directly read the stdout/stderr of your curl unless the execution environment/tool returns the command output back to me (which isn’t happening here).\nIf you paste the exact curl output (including the HTTP status line and headers/body), I’ll diagnose immediately.\n```\n\nSimilarily, getting the tool to actually run a `curl` call against the started app required multiple attempts.\n\nThis behavior raised an obvious question: did it come from my specific Open WebUI tool and configuration stack, or was it a limitation of the model?\n\nFor this run, the following aspects were changed:\n\n`gpt-5.5`\nOne prompt for the whole app.\n\n```\nCreate a repository on /app/projects/python-rest-api with a short README, .gitignore, app/main.py and app/config.json. Implement a REST API service with FastAPI. First, create a requirements file with all required packages. Install the libraries. Then create app/main.py and implement a GET /config route that reads app/config.json from disk and returns parsed JSON. In the JSON file, implement a custom data structure that looks like an LLM model list. Run all commands, show their output.\n```\n\nThe model executed it flawlessly.\n\n```\nDone. The repository was created at /app/projects/python-rest-api with:\n- README.md\n- .gitignore\n- requirements.txt\n- app/main.py\n- app/config.json\n\nImplemented behavior:\n- GET /config in FastAPI\n- reads app/config.json from disk\n- returns parsed JSON\n- handles:\n- missing file with 404\n- invalid JSON with 500\n\nNotes from command output:\n\n- The install step was attempted with python3 -m pip install -r requirements.txt, but this environment does not have pip available: No module named pip\n- File creation succeeded, and python3 -m compileall app successfully compiled app/main.py\n- The generated config.json contains a custom LLM-style model list with entries such as gpt-4o-mini, claude-3-5-sonnet, and llama-3.1-70b-instruct, plus default_model and object: \"list\"\n\nIf you want, I can also provide the exact file contents in a clean code block format.\n```\n\nI could also ask it to start the web server and execute a curl request against the local endpoint.\n\n```\nFrom /app/projects/python-rest-api, start the app on port 9000, then run curl against the config endpoints. Show the output of all commands.\n```\n\nAnswer:\n\n```\nFrom the provided context, the app was started successfully on port 9000 from /app/projects/python-rest-api with Uvicorn.\n\nCommand\npython3 -m uvicorn app.main:app --host 127.0.0.1 --port 9000\n\nOutput\nINFO:     Started server process \nINFO:     Waiting for application startup.\nINFO:     Application startup complete.\nINFO:     Uvicorn running on [http://127.0.0.1:9000](http://127.0.0.1:9000) (Press CTRL+C to quit)\n\nThen curl was run against both config endpoints\n\nCommand\ncurl -i [http://127.0.0.1:9000/config](http://127.0.0.1:9000/config) ; echo '---' ; curl -i [http://127.0.0.1:9000/config/](http://127.0.0.1:9000/config/)\n\nOutput\nHTTP/1.1 200 OK\ndate: Fri, 03 Jul 2026 16:56:45 GMT\nserver: uvicorn\ncontent-length: 656\ncontent-type: application/json\n\n{\"models\":[{\"id\":\"gpt-4o-mini\",\"object\":\"model\",\"owned_by\":\"openai\",\"context_window\":128000,\"pricing\":{\"input_per_1k_tokens\":0.00015,\"output_per_1k_tokens\":0.0006},\"capabilities\":[\"chat\",\"tools\",\"json_mode\"]},{\"id\":\"claude-3-5-sonnet\",\"object\":\"model\",\"owned_by\":\"anthropic\",\"context_window\":200000,\"pricing\":{\"input_per_1k_tokens\":0.003,\"output_per_1k_tokens\":0.015},\"capabilities\":[\"chat\",\"vision\",\"tools\"]},{\"id\":\"llama-3.1-70b-instruct\",\"object\":\"model\",\"owned_by\":\"meta\",\"context_window\":131072,\"pricing\":{\"input_per_1k_tokens\":0.0009,\"output_per_1k_tokens\":0.0009},\"capabilities\":[\"chat\",\"completion\"]}],\"default_model\":\"gpt-4o-mini\",\"object\":\"list\"}\n\n***\nHTTP/1.1 307 Temporary Redirect\ndate: Fri, 03 Jul 2026 16:56:45 GMT\nserver: uvicorn\ncontent-length: 0\nlocation: [http://127.0.0.1:9000/config](http://127.0.0.1:9000/config)\n\nSummary:\n\nGET /config returned 200 OK with the parsed JSON config\nterminal:6351ac...un_command\nGET /config/ returned 307 Temporary Redirect to /config\nterminal:6351ac...un_command\n```\n\nIn this blog post, you learned how to set up a custom coding agent in Open WebUI. Starting from a set of requirements, all configuration steps were shown: a) start custom model creation with a suitable base model from an LLM provider, b) add a custom prompt, c) activate memory, and d) define capabilities and tools. To execute code, an Open Terminal container needs to be added, for which a custom Docker image was built and added to the `docker-compose` file. The agent was tasked with creating a Python REST API with a single endpoint at `/config`. In the first run with a `gpt-5.4-nano` model, the interaction was cumbersome because the model only executed commands after repeated clarification that it could run them. In the second run, with `gpt-5.5` as the base model, two one-shot prompts created all files, started the server process, and ran a `curl` command against the endpoint to test the application. This worked flawlessly, and highlights the importance of using capable models in capable agentic harnesses.", "url": "https://wpnews.pro/news/open-webui-creating-custom-agents", "canonical_source": "https://dev.to/admantium/open-webui-creating-custom-agents-2oa9", "published_at": "2026-10-12 05:30:00+00:00", "updated_at": "2026-10-12 05:39:43.678847+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "large-language-models", "developer-tools"], "entities": ["Open WebUI", "Ollama", "OpenAI", "Docker", "gpt-5.4-nano", "admantium.com"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/open-webui-creating-custom-agents", "markdown": "https://wpnews.pro/news/open-webui-creating-custom-agents.md", "text": "https://wpnews.pro/news/open-webui-creating-custom-agents.txt", "jsonld": "https://wpnews.pro/news/open-webui-creating-custom-agents.jsonld"}}