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)

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
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.

Frequently asked questions

Does Open Ticket AI Runtime require an AI model?

No. The runtime can perform conventional automations such as routing, field updates, and scheduled jobs without AI.

How does the runtime connect to OTOBO?

It typically uses OTOBO's Generic Interface and REST web services with a dedicated technical user.

Can the runtime run on-premise?

Yes. It runs as a Python service or container in your own infrastructure.