Skip to content

Cauce by example

Each example shows the file and the graph it describes, side by side. They add one thing at a time: if you understand the first, the last one has nothing new in it except more lines.

They are all valid files. You can paste them into a .toml and run them through reply2fb cauce validar exactly as they are.

The keys stay in Spanishcuentas, flujos, condicion— because that is what the parser reads. A translated key is a file that does not validate, so here only the explanations are in English.

1. The smallest thing that works

One source, one destination, and a flow between them.

formato_version = 1
credenciales = "ausentes"
[[cuentas]]
handle = "gss"
plataforma = "instagram"
rol = "lectura"
id_remoto = "17841400000000000"
[[cuentas]]
handle = "sumud"
plataforma = "mastodon"
rol = "escritura"
instancia_url = "https://mastodon.cl"
max_por_hora = 4
[[flujos]]
clave = "gss-a-sumud"
origen = "gss"
destino = "sumud"
activa = true
auto_publica = false

That describes this:

gss-a-sumud

gss

instagram · read

sumud

mastodon · write

4 per hour

formato_version and credenciales are mandatory and come first. The first is what gets looked at before trying to understand the rest; the second says whether this is a backup or a description — and whether somebody archives a file believing they can restore from it depends on that.

max_por_hora goes on the RECEIVING account, not on the flow. The limit belongs to whoever receives: if tomorrow three flows point at sumud, the ceiling is still set by Mastodon. On the flow, three routes “of 4 per hour” would do 12.

2. With a filter: recent videos only

One line is added to the flow.

[[flujos]]
clave = "gss-a-sumud"
origen = "gss"
destino = "sumud"
activa = true
auto_publica = false
condicion = { tipo = ["VIDEO"], tag = ["palestina"], desde_dias = 30 }

passes

does not pass:

archived anyway

gss

filter

type: VIDEO

tag: palestina

last 30 days

sumud

archive

The three criteria are met AT THE SAME TIME. “Videos about Palestine from the last 30 days” wants all three things. With an OR, this flow would fire on far more than its author asked for.

And it is fail-closed: an item with no tags does not pass a tag filter. We do not know what it is about, so we do not claim that it is about that.

desde_dias bounds what is EMITTED, not what is archived. The full history still enters the archive — the dotted arrow in the diagram. What this window prevents is a two-year rescue turning into two years of publications.

3. A subscription to a public account

An account that gave permission but cannot hand over an API token. It is read by opening its profile with a browser.

[[cuentas]]
handle = "colectivo"
plataforma = "instagram"
rol = "lectura"
id_remoto = "colectivo"
via = "navegador"
poll_horas = 12

with a token

without a token

colectivo

via = navegador

gss

Graph API

headless browser

opens the public profile

Meta's API

archive

via is explicit and is not inferred from a missing credential. Inferring it would turn a revoked token into scraping a public profile, without anybody deciding so. And without this field in the file, exporting and re-importing turned the subscription into a normal account: it requires a token, it does not have one, and it brings back nothing — silently.

4. An RSS feed

The feed URL goes in id_remoto. It carries no instancia_url, which names the server of a federated account.

[[cuentas]]
handle = "sitio-propio"
plataforma = "rss"
rol = "lectura"
id_remoto = "https://ejemplo.org/feed.xml"
poll_horas = 1
[[flujos]]
clave = "rss-a-telegram"
origen = "sitio-propio"
destino = "canal"
activa = true
auto_publica = false
condicion = { autor = ["Redacción"] }

autor exists for the sources that distinguish author from account: the feed of a site where many people write, or a Telegram channel with bylines. On an Instagram account, author and account are the same thing and the criterion is useless.

5. Your own site as a destination

A web account is a site: it is read from and published to.

[[cuentas]]
handle = "sitio"
plataforma = "web"
rol = "ambos"
id_remoto = "sitio-propio"
[cuentas.sitio]
api_url = "http://127.0.0.1:8181"
sitio_url = "https://ejemplo.org"
uid_servicio = "160451ec-828f-4a5b-b78c-04ff5c44fcab"
tipos = ["post", "articulo"]

api_url

127.0.0.1:8181

posts go out under

uid_servicio's name

the link uses

sitio_url

reply2social

the site

published

whoever reads it

api_url and sitio_url are different on purpose. The first is how this process reaches the site, and it is usually an internal address that does not exist for anybody else. Publishing a link to 127.0.0.1 is publishing a broken link.

It goes on the account and not in the process environment. It used to be a global variable, which meant one installation served one site: with two web accounts, both published to whichever the variable named — the second one to the wrong site, with no error at all.

6. A complete installation

Everything together: two sources, three destinations, four flows.

formato_version = 1
credenciales = "ausentes"
# ── Sources ──────────────────────────────────────────────────────────────
[[cuentas]]
handle = "gss"
plataforma = "instagram"
rol = "lectura"
id_remoto = "17841400000000000"
poll_horas = 6
consentimiento_vigente = "mastodon+telegram"
[[cuentas]]
handle = "prensa"
plataforma = "rss"
rol = "lectura"
id_remoto = "https://ejemplo.org/feed.xml"
poll_horas = 1
# ── Destinations ─────────────────────────────────────────────────────────
[[cuentas]]
handle = "sumud"
plataforma = "mastodon"
rol = "escritura"
instancia_url = "https://mastodon.cl"
max_por_hora = 4
max_en_vuelo = 1
video_como_imagenes = 3
[[cuentas]]
handle = "canal"
plataforma = "telegram"
rol = "escritura"
id_remoto = "-1001234567890"
# ── Flows ────────────────────────────────────────────────────────────────
[[flujos]]
clave = "gss-a-sumud"
origen = "gss"
destino = "sumud"
nombre = "recent videos → Mastodon"
activa = true
auto_publica = false
condicion = { tipo = ["VIDEO"], desde_dias = 30 }
[[flujos]]
clave = "gss-a-canal"
origen = "gss"
destino = "canal"
activa = true
auto_publica = false
[[flujos]]
clave = "prensa-a-sumud"
origen = "prensa"
destino = "sumud"
activa = false
auto_publica = false
condicion = { autor = ["Redacción"] }

VIDEO only,

last 30 days

everything

paused ·

author: Redacción

gss

instagram · 6 h

prensa

rss · 1 h

sumud

mastodon · 4/h

3 frames if video

canal

telegram

Notice that prensa-a-sumud is dotted: it is declared with activa = false. The graph draws what exists, not what runs.

7. A backup, which is a different thing

The same format, with a different header and two extra blocks. It only comes out through the CLI, with cauce exportar --respaldo.

formato_version = 1
credenciales = "cifradas"
[cifrado]
algoritmo = "AES-256-GCM"
huella_clave = "b3f1a9c7"
[[cuentas]]
handle = "gss"
plataforma = "instagram"
rol = "lectura"
id_remoto = "17841400000000000"
credencial_cifrada = "k3JhbmRvbS1ieXRlcy1hcXVpLXF1ZS1uby1zaXJ2ZW4..."

This file does not get committed. It carries the credentials exactly as they are in the database. huella_clave is there so you can know, before attempting a restore, whether this installation has the key they were encrypted with — finding that out at the end of a disaster is too late.

Over HTTP the tokens always come out empty. There is no parameter and no permission that makes the panel hand this over. It is what leaves the invariant “no endpoint returns ciphertext” without exceptions to remember.

What the plan says

Before applying anything, reply2fb cauce importar archivo.toml shows what it would do (the output is in Spanish, as the CLI is):

+ prensa
= gss
= sumud
~ canal (max_por_hora)
+ prensa-a-sumud
= gss-a-sumud
· 'gss' trae anotado un consentimiento: es informativo y el import NO lo
otorga. Un permiso se afirma en el panel, con quién lo afirma y cuándo
· gss-a-sumud: viene activo. El import lo crea PAUSADO igual
cuentas: 1 nueva(s), 2 sin cambios, 1 a actualizar · flujos: 1 nuevo(s), 1 sin cambios
Esto es sólo el PLAN: no se escribió nada. Con --aplicar se aplica.
MarkMeans
+Would be created
=Already there, and left as it is
~It is there, and the file declares something else — which fields are listed
Conflict or failure: the whole plan refuses
·A warning: something the file declares and the import will not apply

~ lists the fields. “The account was updated” says nothing; “(max_por_hora)” reveals that what changes is the tap, which is a decision with consequences for how much goes out per hour.