Guide · Znuny & OTRS migration

Moving off OTRS/Znuny without a big-bang migration

Years of tickets, customers, queues and permissions sit in your Znuny database. Tiqora does not ask you to export them. It runs next to Znuny on the same database, takes over one job at a time, and leaves you a way back at every step until the last one.

The problem with migrating a helpdesk

In December 2020 OTRS AG announced that OTRS 6 had reached end of life, with no security updates after 1 January 2021. That ended the open-source ((OTRS)) Community Edition at version 6.0.x. Znuny picked up the codebase as a community fork. Its first LTS release, 6.0, shipped in January 2021, and the current LTS is Znuny 6.5. The 7.x line, currently 7.3, continues in parallel. Znuny is still mostly Perl and JavaScript running on a classic mod_perl setup.[1–4]

Many teams have run that system for ten years or more. The database holds every ticket and article, customer users and companies, queues, groups and roles, dynamic fields, SLAs, notification rules, GenericAgent jobs and the escalation calendars. Whatever comes next has to take all of that with it.

A classic helpdesk migration usually works like this:

  1. Export everything, map it onto a different data model and import it.
  2. Freeze the old system over a weekend.
  3. Switch every agent, mailbox, notification and integration at once.
  4. Hope nothing important was lost in the mapping.

The risky part is not the export. Everything depends on one moment. Ticket numbers, follow-up threading, history and SLA timers all have to come out right on the first try, and once mail flows into the new system the old one is out of date.

The idea: don't move the data, share it

Tiqora is a clean-room helpdesk built on Python (FastAPI) and React, with no Znuny code inside. It reads and writes the same database as an existing OTRS 6.0.x or Znuny 6.0–7.3 installation, on MariaDB/MySQL or PostgreSQL. You point it at your Znuny database, and both systems work on the same tickets at the same time.

That only works if Tiqora follows a few strict rules while Znuny is still running:

  • No changes to Znuny's schema. Tiqora does not alter Znuny tables during parallel operation. Its migrations only touch its own tables.
  • New state goes into new tables only. API keys, 2FA, drafts, AI settings and similar data live in additive tiqora_* tables. Znuny never reads them.
  • Writes look like Znuny's own. Tiqora allocates ticket numbers with Znuny's lock-free counter algorithm and uses the configured number generator. It writes ticket_history rows in the exact formats Znuny's parsers expect, recomputes the four escalation columns with the same working-time math, and sets the same search-index flags.
  • Background duties stay with Znuny until you hand each one over with its own switch: mail fetching, escalations, notifications, GenericAgent, automatic unlock and pending checks.

How this is tested. A schema-matrix suite loads the real upstream database definitions for each supported release on MariaDB and PostgreSQL. A golden-master suite runs real OTRS/Znuny containers from 6.0.45 up to 7.3.5 next to Tiqora on one MariaDB database, then checks that a full ticket lifecycle produces matching history rows (create, state change, move, priority, owner, note, close). The same suite covers ticket-number interleaving, checksum digits and escalation columns.

At startup Tiqora detects which version your schema belongs to (for example znuny-6.5 or znuny-7.3) and adjusts to it. If it cannot recognise the schema, it refuses to start instead of failing later in production. The detected profile is shown under Admin → System info.

Preferred starting point

Any version from 6.0 to 7.3 works. If you can upgrade first, the docs recommend Znuny 6.5 (LTS) or 7.3 as the peer. OTRS 5 and older need the upstream upgrade to 6.0+ first; Tiqora does not replace that database upgrade.

Step by step

The migration has six phases. You can stay in any phase for as long as you like, from a day to several months. Every phase before the last one can be undone without restoring a backup.

  1. 0BackupVerified dump + staging copy
  2. 1InstallTiqora on the same DB, pilot agents look around
  3. 2Cache syncTiqoraSync add-on or lower cache TTLs
  4. 3Agents moveDaily work in Tiqora, Znuny keeps the daemons
  5. 4Daemons moveOne flag at a time, postmaster last
  6. 5CutoverZnuny off, Tiqora owns the schema

Phases 0–4 and the first half of phase 5 are reversible by configuration. Schema ownership at the end is the only step that needs a backup to undo.

Who owns what, phase by phase

Phase Agents work in Background jobs Database schema
0–2 Znuny (pilot group tries Tiqora) Znuny Znuny + additive tiqora_*
3 Tiqora (Znuny still available) Znuny Znuny + additive tiqora_*
4 Tiqora Moving one by one to Tiqora Znuny + additive tiqora_*
5 Tiqora Tiqora Tiqora (additive migrations only)

Phase 0
Back up, and prove the backup restores

Before anything touches the production database, take a dump and restore it into a scratch database. A backup job that reports success does not prove the restore works. The same restored copy is a good staging environment for trying out phases 1–4.

# MariaDB / MySQL: --single-transaction avoids locking a live Znuny
mysqldump --single-transaction --routines --triggers \
  -h db.example.internal -u backup_user -p \
  znuny_production > znuny_pre_tiqora_$(date +%Y%m%d).sql

# PostgreSQL
pg_dump --format=custom -h db.example.internal -U backup_user \
  znuny_production > znuny_pre_tiqora_$(date +%Y%m%d).dump
On a staging copy

Never start mail fetchers or outbound workers against a copy of production data. They would read your real mailboxes and write to your real customers.

If you stop here

Nothing has changed. Keep the dump until well after the cutover; it is the only way back from the final step.

Phase 1
Install Tiqora next to Znuny

Deploy the tiqora-api and tiqora-worker containers, plus Redis and Meilisearch (both are in the example Compose file). Give Tiqora its own database user with SELECT, INSERT, UPDATE, DELETE on the Znuny database. Do not grant ALTER or DROP, and do not reuse Znuny's credentials. Then point DATABASE_URL at the existing database:

environment:
  DATABASE_URL: mysql+aiomysql://tiqora:…@db.example.internal:3306/znuny_production
  # or: postgresql+asyncpg://tiqora:…@db.example.internal:5432/znuny_production
  # TIQORA_SCHEMA_OWNERSHIP stays unset for the whole parallel period

Create Tiqora's own tables and build the search index:

docker compose run --rm --entrypoint tiqora tiqora-api migrate upgrade
docker compose run --rm --entrypoint tiqora tiqora-api index rebuild

tiqora migrate upgrade only creates tiqora_* tables. The migrations that may touch Znuny's tables sit in a separate chain that stays locked until phase 5. Check Admin → System info to confirm the detected schema profile matches your Znuny version.

If you use PGP or S/MIME, mount Znuny's keyring and certificate directories into the containers. Tiqora uses the same key stores and respects the same SysConfig switches. The customer portal is off by default; turn it on with TIQORA_PORTAL_ENABLED=true.

Now give a small pilot group the Tiqora URL. Technically they can already edit tickets, so tell them clearly that this is an evaluation for now.

If you stop here

Stop the Tiqora containers. Znuny was never touched. The leftover tiqora_* tables do no harm and can be ignored or dropped.

Phase 2
Keep Znuny's cache in step

Znuny caches tickets in memory. When Tiqora writes straight to the database, Znuny does not see the change until the cache expires. The fix is TiqoraSync, a small optional OPM add-on (GPL-3.0, Framework 6.0.x–7.3.x). Install it into the running Znuny:

bin/znuny.Console.pl Admin::Package::Install /path/to/TiqoraSync-1.1.1.opm
# OTRS 6.0: bin/otrs.Console.pl

It adds one Znuny scheduler task that runs every minute. The task reads the invalidation signals Tiqora writes alongside each ticket change and clears those tickets from Znuny's cache. Worst-case staleness in the Znuny UI is therefore about 60 seconds. If you cannot install add-ons, lowering Znuny's cache TTLs is the documented fallback.

Check it: create or edit a queue in Tiqora and confirm it shows up in Znuny without a restart.

If you stop here

Admin::Package::Uninstall TiqoraSync, and you are back to phase 1.

Phase 3
Agents switch, Znuny keeps the daemons

There is no global "writes on" switch. This step is a decision you communicate: tell a team that Tiqora is now their main interface for their queues. Expand team by team. Znuny's daemon keeps fetching mail, sending notifications and running escalations, so nothing about mail flow changes yet.

Tickets edited in Tiqora are stored in Znuny's format with Znuny's history rows, so an agent who opens the same ticket in Znuny sees a normal Znuny ticket.

AI stays manual in this phase

Autonomous AI replies need system.operation_mode = tiqora_primary. The default during parallel operation is parallel, so no automatic customer reply can go out from Tiqora while Znuny still owns the mailbox. AI drafts and summaries that an agent triggers still work. The one exception is Telegram, which Znuny never handles.

If you stop here

Agents go back to the Znuny UI. Whatever they did in Tiqora is already valid Znuny data, so nothing needs to be repaired.

Phase 4
Hand over the daemons, one flag at a time

Each background duty moves on its own. Every Tiqora worker job has a flag in tiqora_settings, off by default, which you switch under Admin → Services. The worker picks up the change on its next tick without a restart. Run each duty on one side only, or work gets done twice: two tickets for one mail, notifications sent twice. Always switch Znuny's side off first, then turn Tiqora's on.

Recommended order:

# Duty Disable in Znuny Enable in Tiqora
1 Escalation sweep EscalationCheck cron task daemon.escalation.enabled
2 Event notifications Ticket::EventModulePost###900-NotificationEvent daemon.notifications.enabled
3 Automatic unlock TicketUnlockTimeout cron task daemon.unlock_timeout.enabled
3 Pending states & reminders TicketPendingCheck cron task daemon.pending_check.enabled
4 GenericAgent GenericAgent cron task + scheduled job executor daemon.generic_agent.enabled
5 Inbound mail (postmaster) MailAccountFetch cron task daemon.postmaster.enabled

Escalations go first because the math is deterministic: a short overlap just computes the same value twice. Inbound mail goes last because a mistake there costs the most. With POP3/IMAP delete-after-fetch, two fetchers can create duplicate tickets or lose mail. Auto-responses do not get a flag of their own; they move with the postmaster. Pending reminders only reach anyone once notifications are on Tiqora's side.

A cron task is switched off in Znuny like this:

bin/znuny.Console.pl Admin::Config::Update \
  --setting-name "Daemon::SchedulerCronTaskManager::Task###MailAccountFetch" \
  --valid 0
# then deploy, and confirm the task is really gone:
bin/znuny.Console.pl Maint::Daemon::Summary
"Done." is not proof

Some Znuny settings are marked required. For those, Admin::Config::Update prints its usual success line and changes nothing. Always check the deployed configuration (Kernel/Config/Files/ZZZAAuto.pm) and Maint::Daemon::Summary. The scheduled GenericAgent executor is usually such a required setting, so plan its handover for the moment you stop the Znuny daemon.

Before taking over notifications, list which events your rules actually listen to:

SELECT ne.name, nei.event_key, nei.event_value
  FROM notification_event ne
  JOIN notification_event_item nei ON nei.notification_id = ne.id
 WHERE ne.valid_id = 1 AND nei.event_key IN ('Events', 'Recipients')
 ORDER BY ne.id;

Rules carried over from older OTRS versions are often bound to the legacy Notification* events. Tiqora emits those too, but a rule bound to an event nobody emits never fires, even while the takeover reports healthy runs. Also set notification.sender_email explicitly so notifications do not go out from the queue's address.

For each duty: disable the Znuny task, set the Tiqora flag, wait at least one interval, and check that the work happened exactly once (one ticket per test mail, one history row per escalation event). Only then move on to the next duty. GenericAgent jobs with NewDelete stay inactive until you also set daemon.generic_agent.allow_delete.

If you stop here

For any duty: set its flag back to 0, let an in-flight Tiqora run finish, and re-enable the Znuny task. Escalation columns re-converge on Znuny's next run. The notification position is kept, so turning it on again later does not resend old events. Mail that Tiqora left on the server is picked up by Znuny's next fetch.

Phase 5
Cutover: Znuny off, schema ownership on

You get here when all six flags have run cleanly in production for a representative period. The cutover runbook has its own preconditions. The most important ones: a freshly tested restore, a maintenance window, and an inventory of everything the Znuny host still does (OS cron jobs, add-ons, calendar jobs, the outgoing mail_queue). Then:

  1. Freeze Znuny's web UI. Put a 503 maintenance response on the nginx vhost in front of index.pl/customer.pl, and leave Tiqora reachable.
  2. Check the flags and stop the Znuny daemon. All six daemon.*.enabled flags must be 1. Stop the daemon and confirm it stays stopped through at least one full interval and the next scheduled job time.
  3. Repoint GenericInterface clients (optional). Only needed if external integrations call Znuny's webservices. An nginx rewrite from nph-genericinterface.pl to Tiqora's /znuny-compat layer covers the common Session* and Ticket* operations over REST and SOAP. Test each operation your integrators use.
  4. Monitor. Watch error rates and per-service status under Admin → Services for at least an hour, or a business day for a cautious rollout.
  5. Set the operation mode on purpose. Switching system.operation_mode to tiqora_primary can activate AI auto-reply rules that were configured but inactive. Review them first.
  6. Take schema ownership. Only now may Tiqora run migrations that touch Znuny's tables. This takes two separate switches:
tiqora ownership status
tiqora ownership enable --confirm "I have shut down Znuny"
# preflight: ticket_history idle ≥ 15 min, Znuny sessions table empty

# then set TIQORA_SCHEMA_OWNERSHIP=1 on every Tiqora process and restart
tiqora migrate upgrade
tiqora ownership orphan-report   # optional, read-only

The first owned migration today only adds three composite indexes. It does not change any rows. Afterwards, archive Znuny's crontab and decommission its frontend. TiqoraSync is retired together with Znuny.

If you stop here

Steps 1–5 are undone by configuration: remove the nginx 503 override, turn the flags you are returning off before restarting the Znuny daemon, and revert the GenericInterface rewrite. If the ownership marker is set but TIQORA_SCHEMA_OWNERSHIP is not, delete the marker rows and nothing has changed. If owned migrations have been applied, the current index-only migration can be stepped back with tiqora migrate downgrade owned@-1. For anything beyond that, the only supported way back is restoring the phase 0 dump.

What is not there yet, and what is out of scope

Tiqora covers everyday agent, customer-portal and admin work. Some things are still missing or narrower than in Znuny. Check this list against how your team actually works (as of 1 October 2026):

Missing

Not built yet

  • Saved searches
  • Configurable dashboard widgets
  • Link overview and link-type admin
  • Admin screens for sessions, SQL box, maintenance, system log
  • Calendar admin and appointment rules
  • Portal print view
Partial

Narrower than Znuny

  • Statistics: fixed reports, not the Znuny stats framework
  • GenericAgent: a subset of search criteria
  • Bulk actions and user preferences
  • GenericInterface edge cases
  • Process conditions and actions
By design

Out of scope

  • OPM package manager
  • Process designer (processes run, but are designed elsewhere)
  • SysConfig edit UI (settings are read from Znuny's SysConfig tables)
  • GenericInterface webservice editor and requester side

The daemon ports also have documented simplifications. Escalation "notify before" uses one fixed window instead of per-SLA percentages. Notifications send by email only and ignore per-agent opt-outs. The postmaster converts HTML mail to plain text more crudely and matches customers by login or email only. The full lists are in the compatibility notes and in the "Uncertainties" sections of the parallel-operation guide. Read them before phase 4; this is exactly where running both systems side by side helps, because you find out on real traffic and can still switch back.

FAQ

Do I need to stop Znuny to try Tiqora?

No. Tiqora runs next to Znuny on the same database and only adds its own tiqora_* tables. Znuny, including its daemon, keeps running until you choose to cut over in phase 5. For a first look without any install, use the live demo. It runs on mock data in your browser.

Which OTRS and Znuny versions are supported?

OTRS 6.0.x (including Centuran ((OTRS)) CE 6.0.x) and Znuny 6.0, 6.1, 6.2, 6.3, 6.4, 6.5, 7.0, 7.1, 7.2 and 7.3. OTRS 5 and older, OTOBO and the commercial OTRS product are out of scope. Upgrade an older install to 6.0+ with the upstream tools first. A schema Tiqora cannot recognise makes it refuse to start instead of misbehaving.

MariaDB/MySQL or PostgreSQL?

Both. The schema-matrix tests load each supported release's real schema on MariaDB and PostgreSQL. The tests against running OTRS/Znuny instances currently use MariaDB only, because Znuny on PostgreSQL is rare. If you run PostgreSQL, rehearse on a staging copy.

What about my OPM add-ons, or ITSM?

Tiqora leaves add-on tables alone, and it has no package manager, so add-ons do not carry over. It provides no ITSM/CMDB module. The GenericInterface compatibility layer does not emulate custom operations such as ConfigItem*, and process actions like ConfigItemUpdate are logged as unsupported.

The cutover runbook asks you to list everything your add-ons and custom jobs actually do and decide for each: replaced, no longer needed, or kept running as a separate service. If an add-on changed Znuny's core tables, try Tiqora on a staging copy first. We have not tested specific third-party add-ons, so we cannot promise any of them works.

Can I go back to Znuny?

Yes, at every step before schema ownership, and without restoring a backup. Stop Tiqora, uninstall TiqoraSync, or set a daemon flag back to 0 and re-enable the Znuny task. The data Tiqora wrote is already in Znuny's format. Once owned migrations have been applied, the supported way back is restoring the dump from phase 0, which is why that step comes last.

Is Tiqora a Znuny fork?

No. It is an independent reimplementation in Python and React, licensed under AGPL-3.0, with no Znuny or OTRS source code in it. Only the optional TiqoraSync add-on is Perl, and it runs inside your Znuny during parallel operation.

See it before you plan anything

Click through the agent, admin and portal UI in the live demo, then read the full runbooks. Questions about your setup are welcome in GitHub Discussions.

The full runbooks

Sources

  1. End of OTRS 6 / ((OTRS)) Community Edition, announced December 2020, no security updates after 1 January 2021: Geeker's Digest; Wikipedia: OTRS
  2. OTRS AG's own statement that the Community Edition is no longer maintained: otrs.com
  3. Znuny release dates and LTS status (LTS 6.0: January 2021; LTS 6.5: March 2023; 7.3: March 2026, not LTS): Znuny roadmap
  4. Znuny's technology stack (Perl, mod_perl) and its continuation of the OTRS Community Edition: github.com/znuny/Znuny