# Abstra Jobs — Examples

Each block is a **complete job stage file**. A job runs on its cron schedule (set in stage settings). See `jobs_guide.md`. Jobs run top-to-bottom; make writes idempotent (the same tick can run again or overlap).

## 1. Scheduled ETL sync — upsert from a connector, idempotently

```python
# Pull records from an external system and upsert into a table. ON CONFLICT makes
# it safe to re-run: existing rows update, new rows insert. (cron e.g. 0 * * * *)
import abstra.tables as at
from abstra.connectors import run_connection_action
from abstra.logging import logger

# run_connection_action returns the action's result directly (the SDK already
# unwraps it) — the exact shape depends on the connector action.
invoices = run_connection_action("erp-2cloud", "list_invoices", {"since": "yesterday"})

for inv in invoices:
    at.run_sql(
        """
        INSERT INTO titulos (external_id, status, valor)
        VALUES ($1, $2, $3)
        ON CONFLICT (external_id) DO UPDATE
        SET status = EXCLUDED.status, valor = EXCLUDED.valor
        """,
        [inv["id"], inv["status"], inv["valor"]],
    )

logger.info(f"Synced {len(invoices)} invoices")
```

## 2. Poll a source for new items + fan-out, with a watermark

```python
# Check an SFTP folder for new files; queue each new one to a tasklet; remember
# the newest timestamp so we never reprocess. (cron e.g. */15 * * * *)
import abstra.tables as at
from abstra.connectors import run_connection_action
from abstra.tasks import send_task
from abstra.logging import logger

state = at.select_one("sync_state", where={"name": "sftp_inbox"}) or {"last_seen": 0}

listing = run_connection_action("sftp", "list_directory", {"path": "/publico"})
entries = listing["entries"]                     # result shape depends on the connector action
new = [e for e in entries if e["type"] == "file" and e["modifyTime"] > state["last_seen"]]

for e in sorted(new, key=lambda e: e["modifyTime"]):
    send_task("new_file", {"name": e["name"], "path": f"/publico/{e['name']}"})
    logger.info(f"Queued {e['name']}")

if new:
    newest = max(e["modifyTime"] for e in new)
    if state.get("id"):
        at.update_by_id("sync_state", state["id"], {"last_seen": newest})
    else:
        at.insert("sync_state", {"name": "sftp_inbox", "last_seen": newest})
```

## 3. Drain a queue on a cadence

```python
# Process tasks that piled up at this (job) stage, in batches, on schedule.
from abstra.tasks import get_tasks
from abstra.logging import logger

processed = 0
for task in get_tasks(limit=500):       # pending tasks at this stage
    with task:                          # lock -> work -> complete / revert on error
        # ... do the work with task.payload ...
        processed += 1

logger.info(f"Processed {processed} tasks this run")
```

## 4. Daily report / digest email

```python
# Summarize yesterday and email it. (cron e.g. 0 7 * * * = 07:00 daily)
import abstra.tables as at
from abstra.messages import send_email

rows = at.run_sql(
    """
    SELECT status, count(*) AS c
    FROM pedidos
    WHERE created_at::date = current_date - 1
    GROUP BY status ORDER BY c DESC
    """
)

items = "".join(f"<li><b>{r['status']}</b>: {r['c']}</li>" for r in rows)
html = f"<h2>Pedidos de ontem</h2><ul>{items}</ul>"

send_email(to="ops@company.com", title="Resumo diário de pedidos", message=html, is_html=True)
```

## 5. Cleanup / archival

```python
# Purge stale rows so tables don't grow unbounded. (cron e.g. 0 3 * * * = nightly)
import abstra.tables as at
from abstra.logging import logger

deleted = at.run_sql(
    "DELETE FROM sessions WHERE created_at < now() - interval '30 days' RETURNING id"
)
logger.info(f"Deleted {len(deleted)} expired sessions")
```

## 6. Reconciliation — compare external vs internal, emit discrepancies

```python
# Nightly: compare a payment provider's view against our table and queue
# mismatches for a human/tasklet to resolve.
import abstra.tables as at
from abstra.connectors import run_connection_action
from abstra.tasks import send_task

remote = run_connection_action("stripe", "list_charges", {"created_gte": "yesterday"})
remote_by_id = {c["id"]: c for c in remote}

ours = at.select("charges", where={"reconciled": False})

for row in ours:
    remote_charge = remote_by_id.get(row["stripe_id"])
    if remote_charge is None:
        send_task("missing_remote", {"charge_id": row["id"]})
    elif remote_charge["amount"] != row["amount"]:
        send_task("amount_mismatch", {
            "charge_id": row["id"], "ours": row["amount"], "theirs": remote_charge["amount"],
        })
    else:
        at.update_by_id("charges", row["id"], {"reconciled": True})
```
