KEPLIN Docs

The scripts SDK

The eight modules of the api_manager SDK — datasources, HTTP, logs, uploaded files, secrets, notifications, workflows and reports — with Python examples.

Inside a script, the whole app is one import away. The SDK is called api_manager and is imported module by module:

from api_manager import db, http, log, files, secrets, notify, workflow, reports

These eight names are the SDK's entire public surface — what isn't here does not exist at run time:

Module What for
db Querying the app's datasources (Oracle, PostgreSQL, MySQL/MariaDB, SQL Server, …).
http HTTP requests to external services, with automatic JSON.
log Writing lines to the run's logs.
files Reading files uploaded through an API.
secrets Reading the app's secrets, decrypted on the server.
notify In-app notifications and emails to the app's users.
workflow Starting workflows, waking waits and deciding human tasks.
reports Generating the app's reports as PDF or Excel.

The editor autocompletes all of this — the names, the parameters and each function's documentation appear as you type.

Dica

The LLM prompt button in the editor's bar opens the complete SDK guide, ready to copy with Copy all. Paste it into Claude, ChatGPT or another assistant, along with what you want the script to do — the assistant then knows the exact contract and won't invent functions that don't exist.

The LLM prompt — the whole SDK contract as a text ready to paste into an AI assistant.
The LLM prompt — the whole SDK contract as a text ready to paste into an AI assistant.

db — the app's datasources

The datasources configured in the app are referenced by name. Two functions:

from api_manager import db

crm = db("Dados CRM")

linhas = crm.query(          # list of dicts (column -> value)
    "select id, nome from contas where cidade = ?", ["Lisboa"]
)
conta = crm.query_one(       # the first row, or None
    "select * from contas where id = ?", [42]
)

params is a positional list. The placeholders are the engine's own, from the datasource:

Engine Placeholders Example
PostgreSQL $1, $2, … where id = $1
MySQL / MariaDB ? where id = ?
Oracle :1, :2, … where id = :1

If the datasource does not exist, the call raises an error (and the run ends in Error, if you don't catch it).

Atenção

Dates on Oracle: avoid passing them as parameters — JSON transport makes the type ambiguous. Prefer the literal in the SQL: TO_DATE('2026-06-25','YYYY-MM-DD').

http — requests to external services

Five verbs, all with automatic JSON — a body dict or list goes out as JSON, and a JSON response arrives already decoded:

from api_manager import http, log

r = http.get("https://api.exemplo.com/clientes")
# r = {"status": int, "ok": bool, "body": <decoded json or text>}
if r["ok"]:
    log("received", len(r["body"]), "customers")

r = http.post(
    "https://api.exemplo.com/leads",
    body={"nome": "Vininha & Filhos", "origem": "keplin"},
    headers={"Authorization": "Bearer abc123"},
)

There are also http.put(url, body, headers), http.patch(url, body, headers) and http.delete(url, headers). r["ok"] is true for 2xx responses — a 404 or a 500 does not raise an exception, check the status yourself.

log — the run's logs

from api_manager import log

log("processing", 42, {"fase": "inicial"})

It takes several arguments, like print — and each call is one line in the run's logs, visible in the Run result panel and in the Runs history. The classic print is captured too, but log is the SDK's canonical form.

The detail of a run — the returned result and the log lines written by the script.
The detail of a run — the returned result and the log lines written by the script.

files — uploaded files

When an app API has an argument of type Upload, the client sends a file and the script receives it in input["args"] as a handle — the bytes stay on disk, not in memory. The files module works on that handle:

from api_manager import files


def main(input):
    f = input["args"]["ficheiro"]      # the upload handle
    texto = files.read_text(f)         # str (utf-8)
    dados = files.read(f)              # bytes
    caminho = files.path(f)            # absolute path on disk
    files.save(f, "ultimo-recebido.csv")   # copies to the script's folder
    return {"nome": f["filename"], "tamanho": f["size"]}

The handle carries filename, mimeType and size — useful for validating before processing.

secrets — the app's secrets

Tokens and keys are stored on the app's Secrets page (side tree, App group), encrypted. The script reads them by key:

from api_manager import secrets

token = secrets.get("STRIPE_KEY")   # -> str | None
if token is None:
    raise RuntimeError("Secret STRIPE_KEY not configured in this app.")

The value is decrypted on the server, at run time — it never reaches the browser and never sits in the code.

The app's Secrets page — the keys that scripts read with secrets.get.
The app's Secrets page — the keys that scripts read with secrets.get.

Atenção

Don't do log(token). Logs stay in the run history — a secret written to a log has stopped being a secret.

notify — notifications and emails

The app has two fixed channels — an in-app one and an email one — which can be enabled in the app's Notifications settings (that is where the SMTP configuration and the sender live). users are usernames of app users; roles expand to every member of the role.

In-app, in real time:

from api_manager import notify

r = notify.send(
    "Report ready",
    subtitle="Monthly reports",
    body="The monthly report is available.",
    users=["joao"],
    roles=None,
    data={"url": "/relatorios/42"},
)
# r = {"recipients": int, "delivered": int}

delivered counts the ones delivered in real time, to whoever is connected; the rest stay in the inbox and arrive when the person signs in.

Email:

r = notify.email(
    "Stock alert",
    to=["chefe@empresa.pt"],      # free-form addresses
    users=None,
    roles=["admin"],              # and/or app users and roles
    text="Stock below the minimum.",
    html=None,
)
# r = {"accepted": [emails], "skipped": [usernames with no email]}

The SMTP send happens asynchronously on the server — the script does not wait. attachments takes attachments with the content in base64 (see reports below for the typical case).

workflow — the app's workflows

A script can start a workflow, wake up whoever is waiting for an event, and decide human tasks. The workflow is referenced by name (or by its stable identifier); key is the key of the record it runs on:

from api_manager import workflow

r = workflow.start("Aprovação de despesa", 42, data={"valor": 1200})
# r = {"instanceId": int}

r = workflow.signal("visto", key=42)
# r = {"woken": int}

abertas = workflow.tasks("joao")
# open tasks for that app user, with the possible decisions

r = workflow.complete(task=17, outcome="aprovar", as_user="joao",
                      data={"nota": "ok"})
# r = {"ok": bool}

Rules that matter:

  • workflow.start returns as soon as the engine reaches the first wait — it never waits for the workflow to finish.
  • workflow.signal without a key wakes every workflow parked on that event; {"woken": 0} is not an error — there may simply be nobody waiting.
  • In workflow.complete, as_user is required (whoever decides goes into the history) and the task must be assigned to that user. The values of outcome are the outputs of the task node — the same ones the person sees as buttons. Don't invent names.

reports — generating the app's reports

The document is generated on the server, with the app's data, from an existing report:

from api_manager import notify, reports

r = reports.render("Facturas do mês", {"mes": "2026-03"})
# r = {"filename", "pages", "format", "mime", "bytes", "base64"}

notify.email(
    "March invoices",
    to=["financeiro@empresa.pt"],
    text="Attached.",
    attachments=[{"filename": r["filename"], "content": r["base64"]}],
)

The optional third argument is the format: "pdf" (the default) or "xlsx". The parameters are the ones the report declares. In the result, bytes comes ready to write to disk and base64 ready to attach to an email.

A complete example

Along the lines of the atualizar_indicadores script of the Customer Management app — it reads the pipeline, writes useful logs and warns when closes are coming up:

from datetime import date, timedelta

from api_manager import db, log, notify


def main(input):
    crm = db("Dados CRM")

    abertas = crm.query_one(
        "select count(*) as n, coalesce(sum(valor), 0) as total "
        "from oportunidades where fase not in ('fechada_ganha', 'fechada_perdida')"
    )

    limite = (date.today() + timedelta(days=7)).isoformat()
    fechos = crm.query(
        "select titulo, data_fecho from oportunidades "
        "where fase not in ('fechada_ganha', 'fechada_perdida') "
        "and data_fecho <= ? order by data_fecho",
        [limite],
    )

    log("pipeline:", abertas["n"], "opportunities /", abertas["total"], "EUR")
    if fechos:
        notify.send(
            "Closes this week",
            body=f"{len(fechos)} opportunities expected to close by {limite}.",
            roles=["comercial"],
        )

    return {
        "oportunidades_abertas": abertas["n"],
        "valor_pipeline": abertas["total"],
        "fechos_proximos_7_dias": len(fechos),
    }

Limits and good practice

  • The result of main must be JSON-serializable; maximum 32 MB.
  • Every run respects the script's Time limit (30 seconds to 10 minutes) — when exceeded, the process is killed and it ends as Timeout.
  • Runs are isolated: don't rely on variables from a previous run. Persist to files in the script's folder or to a datasource.
  • Call log(...) at every relevant stage — it is what you will be reading when something fails at 7:00 on a Sunday.
  • Catch recoverable errors and record them; let the fatal ones through — an uncaught exception marks the run as Error, and that is what you want to see in the history when something is genuinely wrong.
  • Don't print secrets, and always read them from secrets — never write them in the code.

Frequently asked questions

from api_manager import db fails on my computer. That is expected: the api_manager module only exists while running on the platform — that is where it is injected. In the editor you get the full SDK autocomplete; the api_manager.py file you see in the script's folder exists purely for that.

db("name") says the datasource does not exist. The name must be exactly the datasource's name in the app (e.g. Dados CRM, with the capital letter and the space). See the list in the Datasources section of the tree.

notify.email returned accepted but the email never arrived. The SMTP send is asynchronous — accepted means the email went out to the app's email channel. Check the SMTP configuration in the app's Notifications settings and the recipient's spam folder.

Can I use the SDK in a window's validation? Yes — the validation runs in the same environment as the main script, with the same input and the same SDK. See Schedules.