Telegram Agent can be extended with your own Python scripts. A script is a plain file next to the settings that plugs into the product’s pipeline and takes over what the built-in means lack. Dropping the file is enough — no rebuild or restart of the application is needed. This is how you connect a service that has no ready source, assign messages to categories and tags by your own rules, clean and enrich text before publication, or send a comment your own way.
This is a capability for those ready to write code; the other sections of the product do not need scripts, and a script is often ten lines.
Where scripts live
All definitions live in the settings folder /project/apps/telegram/scripts/ — in the same permanent volume as the rest of the settings. One script is one settings file with a numeric name (for example, 1.yml):
id: 1
name: Keyword parsing
description: What this script is for — visible in the Scripts section
isActive: true
timeoutSec: 30
passEnv: [HTTP_PROXY]
env:
API_TOKEN: secret
code: |
import json, sys
ctx = json.load(sys.stdin)
print(json.dumps({"categoryIds": []}))
- name and description — so that half a year later it is clear what this is and why;
- the active flag — a disabled script does not serve the places wired to it: the pipeline behaves as if the wiring were absent, and the journal keeps a “skipped” mark;
- the time limit — how many seconds to wait for the answer (60 by default, the cap is 10 minutes); when it expires, the process is stopped;
- environment variables — constant values for the run: addresses, access keys. You can also list which of the server’s environment variables to pass to the script;
- the code — either right in the settings file, or in a separate
.pyfile next to it: whichever is more convenient. A separate file is read from disk on every run, so an edit in it is visible immediately.
The libraries scripts need are listed in one shared file, scripts/requirements.txt. The application prepares the Python working environment itself on the first run — you do not need to configure it separately. Edits to the files apply on the fly, without restarting the application. The product will not let you delete a script that something references: disconnect it from the wiring first.
Execution rules
The shared rules are set in a separate script-runner.yml file next to the scripts folder and apply on the fly. When the file is missing, the defaults are in effect:
- the master switch — one toggle forbids all runs; every wired place honestly reports that execution is disabled instead of pretending everything is fine;
- the Python program — which exact python to run (
python3by default); - the time limit — 60 seconds per run, the cap is 10 minutes;
- concurrent runs — no more than two processes at once by default, so scripts do not overload the server;
- the output limit — 1 MiB per run; larger data stays in the work folder, and the script returns a link to the file.
Seven wiring points
A script can be a classifier, a collector or a text transform — seven places in the pipeline in total:
Classification — messages get categories and tags on their own:
- Router — the script receives the message and the list of your categories, and returns the ones that fit:
{"categoryIds": [...]}. This is the Script router type in the Routing section. Ids that no longer exist in the catalog are simply dropped; - Tagger — the same thing for tags: the script returns
{"tagIds": [...]}.
Content collection — messages arrive from your own source:
- Source — the script fetches messages from any service itself: it receives the source settings and returns what it found as a list, no more than 500 entries per run. The script keeps its reading positions in its own work folder. Details in the Sources section.
Text transformation — the text is edited on the way:
- Message ingest — the script edits the text of a collected message before it is stored: strip the excess, bring it to a single form;
- Queue item — the script edits a future publication at the moment it enters the queue: extend the text or move it straight to the needed state;
- Publish draft — the script receives the finished text before sending and replaces it with its own. This is how a channel signature, hashtags or links get inserted. Works for both the preview and the publication itself — the text is the same everywhere;
- Comment generation — the script receives the finished comment text before sending and replaces it with its own. Details in the AI Comments section.
Handling of scripts is forgiving and strict. Classification and the ingest and queue edits are forgiving: if a script broke, the message (or queue item) is saved as is, and the failure stays in the journal — one failure must not lose content. Publication and comments are strict: if a script broke or did not return text, the send does not happen, and the attempt can be retried. This way unprocessed text never goes out to the channel. Content collection is a separate case: when the script failed to collect messages, the error is recorded on the source and the next fetch retries — what was already collected is not lost.
How a run works
- every run is a separate Python process: it starts, gives its answer and exits;
- one JSON document arrives on input: the wiring point, the script name, where the run came from, the data and the arguments. It is read with the usual
json.load(sys.stdin); - the answer is JSON too: the last whole object the script printed. Progress messages may be printed — they do not interfere;
- every script has its own work folder, preserved between runs: a convenient place to keep files, reading positions and intermediate results;
- data size is limited: when there is a lot of data, pass not the data itself but a link or a file in the work folder;
- the process receives none of the application’s secrets: only service values and the variables you listed yourself in its settings are available to it.
The Scripts section
The sidebar has a Scripts section — a page for watching runs. Definitions are read here, not edited: editing the file is the change, while creation and test runs go through the API.
- the environment state on top: whether Python is there, whether the library set is assembled, how many runs and failures there were in the last 24 hours, what happened last time;
- the script list: enabled or disabled, where the code comes from, what it is wired to, when it ran last;
- the run history: time, wiring point, where the run came from, the object, duration, exit code and result — or a clear failure reason. Filters by status and wiring point;
- a failure is named in plain words and clear without decoding: “Runtime unavailable” (no working Python or the libraries did not assemble), “Timeout”, “Invalid output” (the script printed no whole JSON), “Output too large”, “Canceled”, “Script execution disabled”, “Script not found”;
- the journal is kept for 30 days and cleans itself up.
A script failure never silently replaces data: for classification it is simply no match, for a text edit the record is saved as is, for the strict points a refused send with the reason. In every case the run stays in the journal with the exact time and error.
External API
The same set of commands can be managed from the outside — through the external API with a key from the settings: view the script list and the environment state, read a definition and the run history, create or change a script, delete it and perform a test run. Routers and taggers of the Script type are also configured through the external API; sources and text transforms are defined in the settings files. Details in the Agent section.
Trust boundary
Scripts are your own code, and they run with the application’s rights: there is no separate isolation between a script and the server. So keep them in version control together with the settings folder and do not run other people’s files. When full isolation is needed — deploy the whole application in a separate container, which is what the standard installation does. A script’s secret variables deserve the same attention as connection passwords.
Read also: Routing, Sources, Content Queue, Publishing, AI Comments.