How I Deployed 90+ AI Agents with Zero Repetitive Clicking
Applied Intermediate 18 min read

How I Deployed 90+ AI Agents with Zero Repetitive Clicking

I built 90+ specialized AI agents on Open WebUI. A bash script handles auth, knowledge upload, profile image, and model creation — no manual UI work.

By J. Martin · · ai ml
Table of Contents

Why Should You Care?

Open WebUI lets you build custom AI agents with system prompts, knowledge bases, and profile images. The problem: every agent takes 8–12 clicks to configure manually. At agent #3 you start making mistakes. At agent #10 you start hating yourself.

I needed 90+ agents across six categories — Technology, Finance, Creative, Wellness, Education, and Mentalism. That meant a repeatable, scriptable pipeline. Here’s the one I built.

The 90+ Agent Categories

Before touching code, I mapped out the agent portfolio:

CategoryCountExamples
Technology18DevOps Advisor, Security Analyst, Cloud Architect
Finance16Portfolio Strategist, Tax Navigator, Options Analyst
Creative14Screenwriter, Brand Voice, Music Composer
Wellness12Sleep Coach, Nutrition Guide, Mental Clarity
Education15Socratic Tutor, Language Partner, Research Librarian
Mentalism12Pattern Recognition, Cognitive Reframe, Dream Analyst

Each agent needs: a system prompt, a profile image, optionally a knowledge base. Doing this through the UI is a maintenance trap — changes require re-clicking everything. A script means the definition lives in version control.

The Pipeline: deploy-agent.sh

The script runs 6 steps in sequence. Let’s walk through each one.

Step 1: Get an Auth Token

Open WebUI exposes a REST API, but every endpoint requires a Bearer token. The first thing the script does is exchange credentials for a session token:

#!/usr/bin/env bash
set -euo pipefail

OWUI_URL="${OWUI_URL:-https://chat.southernsky.cloud}"
OWUI_USER="${OWUI_USER}"
OWUI_PASS="${OWUI_PASS}"

get_token() {
  curl -s -X POST "${OWUI_URL}/api/v1/auths/signin" \
    -H "Content-Type: application/json" \
    -d "{\"email\":\"${OWUI_USER}\",\"password\":\"${OWUI_PASS}\"}" \
    | jq -r '.token'
}

TOKEN=$(get_token)
echo "Auth OK: ${TOKEN:0:20}..."
Auth OK: eyJhbGciOiJIUzI1Ni...

One gotcha: the API has a rate limit around 15 requests per minute on the auth endpoint. If you’re deploying a batch, reuse the token. The script exports TOKEN to the environment and all subsequent functions inherit it rather than re-authenticating.

Step 2: Upload Knowledge Base Files

Some agents get domain-specific knowledge — a finance agent gets market structure docs, a security agent gets CVE categorization guides. Knowledge files are uploaded individually first:

upload_knowledge_file() {
  local file_path="$1"
  local filename
  filename=$(basename "$file_path")

  curl -s -X POST "${OWUI_URL}/api/v1/files/" \
    -H "Authorization: Bearer ${TOKEN}" \
    -F "file=@${file_path};type=text/plain" \
    | jq -r '.id'
}

FILE_ID=$(upload_knowledge_file "./knowledge/finance-market-structure.txt")
echo "Uploaded file: ${FILE_ID}"
Uploaded file: f_8a3d1c9e2b4f

The upload returns a file ID. You collect these IDs into an array before creating the knowledge collection.

Step 3: Create a Knowledge Collection

Individual files get grouped into a named collection. The collection ID is what you attach to the agent:

create_knowledge_collection() {
  local name="$1"
  local description="$2"
  shift 2
  local file_ids=("$@")

  # Build the file_ids JSON array
  local ids_json
  ids_json=$(printf '%s\n' "${file_ids[@]}" | jq -R . | jq -s .)

  curl -s -X POST "${OWUI_URL}/api/v1/knowledge/" \
    -H "Authorization: Bearer ${TOKEN}" \
    -H "Content-Type: application/json" \
    -d "{
      \"name\": \"${name}\",
      \"description\": \"${description}\",
      \"file_ids\": ${ids_json}
    }" | jq -r '.id'
}

COLLECTION_ID=$(create_knowledge_collection \
  "Finance Core" \
  "Market structure, options theory, portfolio management" \
  "$FILE_ID")
echo "Collection: ${COLLECTION_ID}"
Collection: k_7f2e9a1d5c8b

Agents without knowledge bases skip steps 2 and 3 entirely — the collection field in the final payload is just left empty.

Step 4: Build the Profile Image

This is where most tutorials stop, because profile images are non-trivial. Open WebUI stores them as base64-encoded WebP in a SQLite column. If you try to upload a 1024x1024 PNG, you will crater the database performance — the base64 alone is ~1.4MB, and Open WebUI loads it on every workspace render.

The correct pipeline:

prepare_profile_image() {
  local source_png="$1"
  local output_webp="/tmp/agent_profile_$$.webp"

  # Resize to 256x256, convert to WebP
  convert "${source_png}" \
    -resize 256x256^ \
    -gravity center \
    -extent 256x256 \
    -quality 85 \
    "${output_webp}"

  # Base64 encode (no line breaks)
  base64 -w 0 "${output_webp}"
}

PROFILE_B64=$(prepare_profile_image "./images/portfolio-strategist.png")
echo "Image encoded: ${#PROFILE_B64} bytes"
Image encoded: 28432 bytes

28KB base64 vs 1.4MB — that’s the difference between a responsive UI and one that lags on every page load. The images were generated first with Imagen 4.0 at 1024x1024, then batch-resized through this pipeline.

Step 5: POST to Create the Model

Now everything comes together. The agent definition is a JSON payload that includes the system prompt, the knowledge collection ID, and the base64 profile image:

create_agent() {
  local agent_id="$1"
  local agent_name="$2"
  local system_prompt="$3"
  local profile_b64="$4"
  local collection_id="${5:-}"

  # Build knowledge array conditionally
  local knowledge_json="[]"
  if [[ -n "$collection_id" ]]; then
    knowledge_json="[{\"type\":\"collection\",\"id\":\"${collection_id}\"}]"
  fi

  curl -s -X POST "${OWUI_URL}/api/v1/models/create" \
    -H "Authorization: Bearer ${TOKEN}" \
    -H "Content-Type: application/json" \
    -d "{
      \"id\": \"${agent_id}\",
      \"name\": \"${agent_name}\",
      \"base_model_id\": \"grok-3\",
      \"params\": {
        \"system\": \"${system_prompt}\"
      },
      \"meta\": {
        \"profile_image_url\": \"data:image/webp;base64,${profile_b64}\",
        \"knowledge\": ${knowledge_json},
        \"suggestion_prompts\": []
      }
    }" | jq -r '.id'
}

CREATED_ID=$(create_agent \
  "portfolio-strategist" \
  "Portfolio Strategist" \
  "You are a CFA-level portfolio analyst..." \
  "$PROFILE_B64" \
  "$COLLECTION_ID")
echo "Created: ${CREATED_ID}"
Created: portfolio-strategist

One critical detail: the id field is alphanumeric plus underscores only. No hyphens. Open WebUI silently rejects hyphens in model IDs, and the failure mode is confusing — the API returns 200 but the agent never appears. The script validates IDs before sending:

validate_agent_id() {
  local id="$1"
  if [[ ! "$id" =~ ^[a-z0-9_]+$ ]]; then
    echo "ERROR: Agent ID '${id}' contains invalid characters" >&2
    exit 1
  fi
}

Step 6: Cleanup Temp Files

The script creates temporary WebP files during image encoding. Always clean up:

cleanup() {
  rm -f /tmp/agent_profile_$$.webp
  echo "Cleanup complete"
}
trap cleanup EXIT

The trap on EXIT runs cleanup regardless of whether the script succeeds or fails. This matters when you’re running batch deploys — a crash halfway through shouldn’t leave 40 temp files sitting around.

Running a Full Batch Deploy

With the functions defined, deploying a batch looks like this:

#!/usr/bin/env bash
# deploy-batch.sh — deploy all agents in agents.json

TOKEN=$(get_token)
export TOKEN

jq -c '.[]' agents.json | while read -r agent; do
  ID=$(echo "$agent" | jq -r '.id')
  NAME=$(echo "$agent" | jq -r '.name')
  PROMPT=$(echo "$agent" | jq -r '.system_prompt')
  IMAGE=$(echo "$agent" | jq -r '.image')

  echo "Deploying: ${NAME}..."
  PROFILE_B64=$(prepare_profile_image "./images/${IMAGE}")
  create_agent "$ID" "$NAME" "$PROMPT" "$PROFILE_B64"
  echo "  OK"

  # Respect rate limit
  sleep 5
done

The sleep 5 between creates isn’t optional — OWUI rate-limits the model create endpoint too. Without it, you’ll start getting 429s around agent #15.

Full batch output for one category:

Deploying: Portfolio Strategist...
  OK
Deploying: Options Analyst...
  OK
Deploying: Tax Navigator...
  OK
Deploying: Retirement Planner...
  OK
[...12 more agents...]
Finance category complete: 16 agents deployed

The Grok API Backend

One important architectural note: these agents run on Grok (xAI’s API), not local Ollama. The base_model_id in the payload is grok-3, which OWUI routes through its Grok API connection.

This was a deliberate choice. Local Ollama models are fast and free, but for a multi-user platform serving real customers, you want the reliability and reasoning quality of a hosted API. The agents handle finance analysis, legal concepts, and medical information — domains where a smaller local model’s hallucinations are genuinely risky.

The tradeoff: Grok API usage costs real money. The system prompt engineering is where you control this. Tight, focused prompts that guide the model to answer concisely keep token consumption down.

Updating an Existing Agent

When you need to update a system prompt, you don’t recreate — you PATCH:

update_agent_prompt() {
  local agent_id="$1"
  local new_prompt="$2"

  # First fetch current metadata so we don't wipe other fields
  local current
  current=$(curl -s "${OWUI_URL}/api/v1/models/${agent_id}" \
    -H "Authorization: Bearer ${TOKEN}")

  local current_meta
  current_meta=$(echo "$current" | jq '.meta')

  curl -s -X POST "${OWUI_URL}/api/v1/models/${agent_id}/update" \
    -H "Authorization: Bearer ${TOKEN}" \
    -H "Content-Type: application/json" \
    -d "{
      \"params\": {\"system\": \"${new_prompt}\"},
      \"meta\": ${current_meta}
    }"
}

The fetch-before-update pattern is critical. The OWUI update API merges only top-level keys, not nested ones. If you send meta: { profile_image_url: ... } without including knowledge and suggestion_prompts, those fields get silently wiped. Always fetch the current state, modify what you need, and send the full object back.

What You Learned

  • Open WebUI’s REST API covers the full agent lifecycle: auth, file upload, knowledge collection, model creation, and update
  • Profile images must be 256x256 WebP before base64 encoding — larger images cause UI performance degradation
  • Model IDs must be alphanumeric + underscores only; hyphens are silently rejected
  • Rate limits on both auth (~15/min) and create endpoints require explicit delays in batch scripts
  • Fetch-before-update is mandatory on OWUI’s model update endpoint to avoid silently wiping nested metadata fields