Skip to content
← Back to projects

Case study

Movidesk → Notion sync

A Python job that keeps a Notion board in step with the Movidesk help-desk and raises Telegram alerts for tickets falling through the cracks. Built for my own team's workflow as an IT infrastructure analyst.

Movidesk
open tickets
→
sync.run()
create · update · archive
→
Notion
the team board
Telegram
alerts, once

The problem

Support tickets live in Movidesk, but the team tracked equipment and "who has what" on a Notion board that was updated by hand. Every ticket that touched a piece of equipment had to be copied across; tickets that came in with no assignee were easy to miss; and the equipment status (busy / free) drifted out of date within a day.

What it does, each run

  1. Pull every open ticket from Movidesk, paging through all results.
  2. For a ticket with an assignee and a linked asset, create or update its Notion page.
  3. For a ticket that is unassigned, has no asset and matches a keyword (notebook, meeting room, microphone, …) → send one Telegram message.
  4. Archive Notion pages whose ticket has been closed or has left Movidesk.

A second mode (--mode equipamentos) reuses the same ticket data to mark each asset Ocupado / Disponível.

Decisions & trade-offs

Idempotent sync, not a one-way push
Each run first indexes the Notion side by ticket number, then decides create vs update vs archive. Running it twice changes nothing the second time; a closed ticket's card is archived, not left behind.
Alert once, with a tiny state file
A NotifiedStore keeps a set of tags in a JSON file. Without it, every run would re-ping Telegram about the same ticket. A corrupt file just starts fresh.
I/O passed in as dependencies
sync.run() takes the Notion client, the Telegram sender and the state store as arguments. That is the whole reason it has 16 tests that drive it with fakes and assert the create / update / archive / alert behaviour — no network, no mocking of requests.
Retry + backoff at the session level
Movidesk and Notion both return 429 under load. Onerequests.Session with a urllib3 retry policy handles that for every call, instead of a try/except around each request.
Config — including the keyword list — from the environment
Which tickets "matter" is a moving target. KEYWORDS is an env var, so tuning it never needs a code change or a redeploy.
A --dry-run flag
A job that writes to a shared team board should let you see the plan first. Dry-run logs every create / update / archive it would do and touches nothing.

Where it landed

  • The manual copy step is gone; the board reflects Movidesk on every run.
  • Unassigned tickets that match a keyword surface in Telegram within one run.
  • 16 pytest tests over the pure helpers, the state store and the orchestration; CI on every push.
  • Refactored from one 250-line dated script into a small package (config, http_client, movidesk,notion, telegram, state, tickets,sync).

Stack

  • Python 3.12
  • requests (Session + urllib3 Retry)
  • python-dotenv
  • Movidesk REST API
  • Notion API
  • Telegram Bot API
  • pytest
  • GitHub Actions

What I'd do next

Validate the Notion database's property schema on startup (fail fast with a clear message instead of a 400 mid-run); structured logging to a file for the scheduled runs; and package it so it installs with pip and runs as a console command.