One bot (inemaccbot) that knows nothing about video, and two domains (promoavatar, promoavatar3) that know nothing about queues. This page has the diagram for each one, the configuration file map, and the “I want to change X, where do I make the change?” table.
The rule that underpins the design is written at the top of the src/fluxos/runtime.ts: the orchestrator doesn’t do the work, and the worker doesn’t make decisions. THE SIX STEPS OF montar-reel.py done/failed; the runtime reads this and chooses the next phase — doing so within the same ack transaction, so “phase completed” and “next phase queued” never exist separately.
Measured in seconds. Classifies the message, validates it against the registry, and queues it. If the agent for the interpret If it hallucinates a command that doesn’t exist, the response is to refuse — closed catalog.
SQLite in WAL mode. Five resource classes with independent concurrency, so a render doesn't take a download's turn. Lease with heartbeat: if the process crashes, the job returns to the queue instead of disappearing.
Two types of jobs. kind: agent calls a model with a prompt; kind: function is deterministic TypeScript code. Today, only the text phase uses a model — avatar and reel are functions.
This is the boundary that lets the system grow without getting bloated. The bot knows about queues, gates, retries, and delivery. The domain knows about audiences, prompts, templates, and the rendering engine. They meet in exactly two files.
config/fluxos.json says which folder to load; the flow.ts validates the flow.json that came from there and rejects anything malformed before any job exists.lives2), never by path. The bot is the only one that knows where this lives on disk (src/dominio/destinos.ts). A new channel appears just by creating the folder yt-pub-livesN — without editing or recompiling the bot.
| File | Read when | Need to restart? |
|---|---|---|
config/fluxos.json | when the process boots | Yes — and check that the queue is empty first: restart kills in-flight renders |
config/skills.json | when the process boots | Yes |
flow.json of the domain | when each flow is created | No |
prompts/*.md of the domain | when it's created — the text is frozen within the flow | No (but an existing workflow keeps the old text) |
templates/*.json | at render time, through preparar.py | No |
.env | at boot | Yes |
A flow isn't a script that runs from top to bottom. It's a definition that is frozen when it's created, then advances phase by phase, with the state in the database. This is what lets you restart the process midway and pick up where it left off.
flow.json| Field | What’s intentionally missing | Values |
|---|---|---|
id | phase name — appears in /status | texto, gerar, baixar, reel… |
escopo | 1 job per flow, or 1 job per target | fluxo · alvo |
fila | resource class — defines concurrency | texto io render navegador cpu |
kind | calls a model, or runs code | agent · function |
tarefa | which bot task runs | fluxo-agente, heygen.gerar, reel.montar… |
prompt | domain file (only in kind: agent) | prompts/fase1-3versoes.md |
variantes | alternative prompts, swapped by | prompt=<nome> | {"viral": "prompts/fase1-viral.md"} |
opcional | the phase only runs if the flag was requested | api estudio creditos navega |
pausa_apos | human gate after the phase | true |
espera | external work polling | {intervalo, timeout} in seconds |
max_tentativas | retry before marking as failed | 2, 3 |
The format is 12 targets (one per audience), prefix A, seven phases, four of which are alternative optional routes for producing the same avatar.
texto → estudio → baixar → reel.The reel engine—one engine, N domains <publico>-<tipo>: 12 audiences × 3 types = 36 targets, prefix C. It gained prompt variants, CTA by variant, and the layout map — and lost the route navega-avatar.
| promoavatar | promoavatar3 | |
|---|---|---|
| prefix | A | C |
| target | <publico> — 12 | <publico>-<tipo> — 36 |
| phases | 7 (4 optional routes) | 6 (3 optional routes) |
route | navega | has it (the agent operates the browser) | doesn’t have |
| prompt variants | no | manifesto, viral |
| CTA | engine default | cta: {padrao, viral} by variant |
| text phase model | opus pinned in the flow.json | default bot profile |
| reel timeout | 10800s (3h) | 21600s (6h) |
SOBREPOSIÇÕES e IMAGENS), must be replicated in every prompt in both repos. Nothing in the system enforces this.
The phase reel doesn’t render anything: it builds a command line and launches a detached process. All rendering lives in Python, in the domain repo — and one domain can point to another domain’s engine with motor_repo, instead of copying the scripts.
The table that answers 90% of questions from people who arrive at the system. The right-hand column says whether the process needs to be restarted.
| I want to… | I edit | Restart? |
|---|---|---|
| Change the text the AI writes | <dominio>/prompts/fase1-*.md | no |
| Add an audience / target | <dominio>/flow.json → alvos | no |
| Change the audience's channel | <dominio>/flow.json → alvos.<alvo>.canal | no |
Create a new channel (lives7) | create the folder ~/projetos/yt-pub-lives7/imports/videos | no |
| Change the avatar or voice | <dominio>/flow.json → avatar_id, voice_id, engine | no |
| Change the reel’s look | <dominio>/templates/*.json | no |
| Change which layout each format uses | <dominio>/templates/mapa.json | no |
| Lock in a layout for an audience | flow.json → alvos.<alvo>.template | no |
| Change the closing clip | <dominio>/cta/*.mp4 + flow.json → cta | no |
| Create a prompt variant | flow.json → fases[texto].variantes + the new one .md | no |
| Add a phase to the pipeline | flow.json → fases (the task must already exist in the bot) | no |
| Add a task new | src/fila/tarefas/ + register in tarefas/index.ts | yes |
| Register a new domain | config/fluxos.json | yes |
| Register a step skill | config/skills.json + prompts/<nome>.md | yes |
| Change a queue’s concurrency | src/fila/filas.ts → CONCORRENCIAS | yes |
| Change the HeyGen key / token | .env | yes |
| Change the help shown in chat | <dominio>/HELP.md | no |
This is the test that the boundary is in the right place. If your new domain requires editing TypeScript, either it needs a task that doesn’t exist or the boundary has leaked.
# 1. the domain repo, alongside the bot mkdir ~/projetos/meudominio && cd ~/projetos/meudominio # 2. flow.json — targets, phases, and the engine for another repo cat > flow.json <<'JSON' { "nome": "meudominio", "prefixo": "M", "versao_def": 1, "motor_repo": "promoavatar3", "templates_dir": "templates", "template": "empilhado-capa", "alvos": { "ep01": { "canal": "lives2", "fecho": "..." } }, "fases": [ { "id": "texto", "escopo": "fluxo", "fila": "texto", "kind": "agent", "tarefa": "fluxo-agente", "prompt": "prompts/fase1.md", "pausa_apos": true }, { "id": "reel", "escopo": "alvo", "fila": "render", "kind": "function", "tarefa": "reel.montar" } ] } JSON # 3. register it in the bot — the ONLY step that requires a restart # config/fluxos.json: {"command":"meudominio","repo":"meudominio",...} # 4. test WITHOUT spending an avatar or render /meudominio assunto de teste | sombra
/fila and check whether there’s a job running. Restart kills an in-flight render — and a reel with a 6h timeout starts over from scratch.
These aren’t implementation details—they’re the choices that prevented entire classes of bugs and apply to any agent-based queue system.
The worker runs a job and marks it done/failed. The runtime decides the next phase inside the same transaction of the ack. Separating these is what eliminated the duplicate dispatch from the previous version.
The worker runs and marks /refazer reproduces exactly what ran.
The agent that interprets free text may hallucinate a command. Its output is validated against the registry before to become a job: a command that doesn’t exist is a refusal, not an attempt.
Pipeline with state per phase and per target, definition frozen at creation, pause for you to do your part ( IMAGENS and made up its own prompts, the fix wasn’t to write the rule in bold — it was to move image generation inside the preparar.py. Only a script, exit code, or removed path changes behavior.
pausa_apos exists where the next step costs money or hours. Reviewing 36 scripts in chat is cheap; redoing 36 avatars isn’t.
The original domain. lives2; only the bot knows this is ~/projetos/yt-pub-lives2/imports/videos. A channel list duplicated N times drifts; in one place, it doesn't.
Phase barrier, job preemption, global agent limit, and multi-user support. Each omission has a documented trigger for reconsideration. A system that starts with everything is a system no one finishes.