Skip to content

Open Ticket AI Runtime – Python Automation for OTOBO

In this guide: What the OTAI Runtime is, how its pipe system works, and how it connects to OTOBO for custom automation — including non-AI workflows.

Related: Generic Agent · Web Services · REST API · OpenTicketAI product overview · OTOBO AI features

When built-in tools like the Generic Agent or Process Management reach their limits, you need a way to express richer logic: fetch tickets, evaluate conditions, call external APIs, update fields, and repeat — all under your control and on your infrastructure.

Open Ticket AI Runtime (OTAI Runtime) is an open-source, Python-based automation engine for exactly that. Despite the name, it is not limited to AI. You can use it for classical ticket routing, field updates, scheduled jobs, and integrations — and add AI classification or summarization only where it helps.

Source code: github.com/Softoft-Orga/open-ticket-ai (LGPL-2.1)


  1. What OTAI Runtime does
  2. Pipes: if this, then do that
  3. How it connects to OTOBO
  4. Quick start
  5. Example: classify and route tickets
  6. Extend with Python plugins
  7. When to use OTAI vs built-in automation
  8. Further reading

OTAI Runtime runs as a containerized Python service (Docker or pip install) between your helpdesk and any logic you define. For OTOBO, it typically:

  1. Ingests tickets — via the Generic Interface (Web Services) using a dedicated technical user.
  2. Runs a pipeline — a sequence of pipes that pass context from step to step.
  3. Executes actions — update queue, priority, dynamic fields, add notes, call external systems, or run ML models locally.
  4. Logs everything — each pipe result is traceable for auditing and debugging.

Because processing stays on-premise, ticket content does not need to leave your network — an important difference from cloud-only automation tools.

flowchart LR
    OTOBO["OTOBO\n(Generic Interface)"]
    RT["OTAI Runtime\n(Python pipelines)"]
    AI["Optional:\nlocal ML / LLM"]
    EXT["Optional:\nexternal APIs"]

    OTOBO <-->|REST| RT
    RT --> AI
    RT --> EXT
    RT -->|update tickets| OTOBO

The core idea is pipes: small, composable processing units wired together in YAML. Each pipe receives a shared context (results from earlier steps), runs its task, and produces a PipeResult for the next step.

Think of it as if this, then do that, chained:

PatternPipe typeBehavior
When (trigger)IntervalTrigger, ExpressionPipeRun only every N seconds, or when a condition is true
If (guard)ExpressionPipe with fail()Stop the pipeline when a check fails
Then (action)FetchTicketsPipe, UpdateTicketPipe, AddNotePipeRead or change tickets in OTOBO
Else / skipSimpleSequentialRunnerRun run only when on succeeded

Atomic steps with one job — for example fetch tickets from a queue:

- id: fetch_tickets
use: base:FetchTicketsPipe
injects:
ticket_system: otobo_znuny
params:
ticket_search_criteria:
queue:
name: Incoming
limit: 10

A CompositePipe runs child steps in order and merges their results — ideal for full workflows:

- id: ticket_workflow
use: base:CompositePipe
steps:
- id: fetch
use: base:FetchTicketsPipe
injects: { ticket_system: otobo_znuny }
params:
ticket_search_criteria:
queue: { name: Incoming }
limit: 10
- id: guard
use: base:ExpressionPipe
params:
expression: >
{{ fail() if (get_pipe_result('fetch', 'fetched_tickets') | length) == 0 else 'ok' }}
- id: update
use: base:UpdateTicketPipe
injects: { ticket_system: otobo_znuny }
params:
ticket_id: "{{ get_pipe_result('fetch', 'fetched_tickets') | first | attr('id') }}"
updated_ticket:
queue:
name: Support

SimpleSequentialRunner implements classic if-then logic: execute run only when on succeeds (for example, when an interval trigger fires):

- id: run-when-triggered
use: base:SimpleSequentialRunner
params:
on:
id: gate
use: base:IntervalTrigger
params: { interval: PT60S }
run:
id: do-something
use: base:ExpressionPipe
params: { expression: Triggered run }

Pipes share data through Jinja2 templates. Use get_pipe_result('pipe_id', 'data_key') to read output from earlier steps and has_failed('pipe_id') to branch on errors.

For background polling (daemon-style loops), SimpleSequentialOrchestrator repeats its steps endlessly — useful for continuous ticket processing.


OTAI Runtime talks to OTOBO through the Generic Interface, the same mechanism described in Web Services and the REST API reference.

Typical setup:

  1. Technical user — create an agent such as open_ticket_ai with only the permissions your pipeline needs (ro, move_into, priority, note, etc.).
  2. Web service — import the bundled OpenTicketAI.yml template from the GitHub repository. It exposes restricted REST operations (ticket-search, ticket-get, ticket-update, ticket-create) and maps all requests to the technical user.
  3. Runtime config — point OTAI at your OTOBO URL and credentials via environment variables and config.yml.

The OTOBO-side steps mirror a standard Web Services integration; the runtime adds the Python pipeline layer on top.

Detailed setup: OTOBO / Znuny plugin setup (Open Ticket AI docs)


Terminal window
pip install open-ticket-ai
# OTOBO/Znuny connector and optional local ML plugins
pip install open-ticket-ai otai-otobo-znuny otai-hf-local
Terminal window
docker pull openticketai/engine:latest
services:
open-ticket-ai:
image: openticketai/engine:latest
ports:
- "8080:8080"
environment:
OT_AI_CONFIG: /app/config.yml
volumes:
- ./config.yml:/app/config.yml:ro

Clone the repository for development:

Terminal window
git clone https://github.com/Softoft-Orga/open-ticket-ai.git
cd open-ticket-ai
uv sync
uv run -m pytest

The same pipe model supports AI and non-AI steps. A classification pipe can sit between fetch and update:

- id: classify
use: otai_hf_local:HFLocalTextClassificationPipe
params:
model: bert-base-german-cased
text: "{{ get_pipe_result('fetch', 'fetched_tickets') | first | attr('subject') }}"
- id: update
use: base:UpdateTicketPipe
injects: { ticket_system: otobo_znuny }
params:
ticket_id: "{{ get_pipe_result('fetch', 'fetched_tickets') | first | attr('id') }}"
updated_ticket:
queue:
name: "{{ get_pipe_result('classify', 'predicted_queue') }}"

Replace the classification pipe with any other pipe — keyword matching, HTTP calls, database lookups — and the workflow structure stays the same.


Every pipe is a Python class registered in the pipe registry (base:FetchTicketsPipe, base:UpdateTicketPipe, …). You can:

  • Configure existing pipes in YAML (no code required for many automations).
  • Install community plugins from the plugin marketplace.
  • Write custom pipes in Python when you need proprietary business logic, then reference them with use: your_plugin:YourPipe.

The runtime uses dependency injection, structured logging, and pytest-based testing — see the developer documentation on openticketai.com.


NeedBuilt-in OTOBOOTAI Runtime
Simple rules (queue, priority, time)Generic AgentUsually overkill
Guided agent/customer flowsProcess ManagementDifferent purpose
REST integration with external systemsWeb ServicesOTAI uses this under the hood
Multi-step logic, Python, ML, custom APIsLimitedOTAI Runtime
On-premise AI without cloud APIsNot built inOTAI Runtime + local model plugins
Fully custom automation without AIGeneric Agent or scriptsOTAI Runtime (pipes, no ML required)

License: LGPL-2.1-only — suitable for extending and integrating in enterprise environments while keeping custom pipe code under your control.