Skip to content

Documentation navigation

Database triggers

Connect your own Postgres table with a reviewed signing sender and local tests.

#Start an agent when a database row changes

Connect a table in your own Postgres or Supabase database. A database trigger sends selected row fields to mAIvn after the transaction commits. mAIvn verifies the signature, selects matching table and operation declarations, and runs your agent. Your application's ordinary database user never needs the signing secret.

#1. Create the connection

In Studio, open Config → Triggers → Connections → Add a connection. In the Developer Portal, open your project's Integrations → Connections → Add connection. Choose Database changes and save the connection ID, callback address and one-time signing secret.

Without either UI, use the same connection lifecycle from a terminal:

Shell
maivn connections create --project-id PROJECT --email YOU@example.com --name "Order changes" --kind database_webhook --secret-file ~/.maivn-private/orders.json

Use your project ID and email. The command prompts privately for your password and writes the receipt into a new owner-private file. See terminal connection setup for remote origins and secret-file requirements. The signing secret is separate from the project's MAIVN_API_KEY.

For a local platform, open a second terminal and run:

Shell
maivn dev-tunnel --connection-id conn_YOUR_CONNECTION

Use the resulting HTTPS receiver URL for the database sender. Keep the tunnel open while testing. A newly allocated hostname can take time to resolve; verify it is reachable before writing a test row. The tunnel uses a free hosted service and an open-source client; no paid account is required.

#2. Prepare your database

The supplied sender is tested with Postgres 17 and pg_net 0.20.4, with pgcrypto installed in the extensions schema. It uses the Supabase row-change payload shape. Supabase's dashboard webhook with a static secret header does not calculate the required signature.

Your database owner prepares these prerequisites and reviews the sender plan. The helper does not install extensions, change server settings, or change shared pg_net permissions. It refuses the mAIvn platform schema, views, partitioned parents, nonexistent columns, and unsafe public or additional queue grants.

On a new isolated development database only, its owner can prepare the extensions and private queue permissions with:

SQL
CREATE SCHEMA IF NOT EXISTS extensions;
CREATE EXTENSION pgcrypto WITH SCHEMA extensions;
CREATE EXTENSION pg_net;
REVOKE ALL ON SCHEMA net FROM PUBLIC;
REVOKE ALL ON ALL TABLES IN SCHEMA net FROM PUBLIC;
REVOKE ALL ON ALL SEQUENCES IN SCHEMA net FROM PUBLIC;
REVOKE ALL ON ALL FUNCTIONS IN SCHEMA net FROM PUBLIC;

pg_net must also be loaded and its background worker running for this database. Follow the pg_net documentation for your installation. Do not apply those shared permission revokes to an existing database without reviewing its current users and integrations. The helper stops instead of silently removing their access. Existing queue grants also require review before adding another sender to the same database.

On stock Supabase, pg_net is owned by supabase_admin and grants PUBLIC access to its queue and functions, which the sender refuses. Enable pg_net from the Supabase dashboard, then have a superuser revoke those PUBLIC grants and hand pg_net's objects to the installing owner, before a non-superuser (CREATEROLE) owner can install the sender.

The installer needs permission to create the dedicated role and private schema, transfer the function's ownership, and attach a trigger to the selected table. Keep driver query-parameter logging disabled. Use an ordinary source table and an explicit list of fields you intend to send, such as id and status; unlisted fields and columns added later stay out of the payload.

#3. Review and install the sender

The SDK's postgres_sender_plan generates reviewable SQL without a secret. Its install method accepts an asyncpg-compatible connection and binds the secret as a separate parameter inside the installation transaction. No partially installed source trigger survives a failed installation.

Save this as setup_database_sender.py in your SDK project. It prompts for your database target and asks you to review the plan before connecting. Use the table and receiver URL belonging to your own database connection.

Python
import asyncio
from getpass import getpass

import asyncpg
from maivn.postgres import postgres_sender_plan


async def main():
    host = input("Your database host: ").strip()
    port = int(input("Database port [5432]: ").strip() or "5432")
    database = input("Database name: ").strip()
    user = input("Database owner username: ").strip()
    table = input("Source table (for example public.orders): ").strip()
    columns = input("Fields to send (for example id,status): ").split(",")
    receiver = input("Full HTTPS connection receiver URL: ").strip()
    plan = postgres_sender_plan(
        table=table,
        columns=[column.strip() for column in columns],
        receiver_url=receiver,
    )
    print(plan.prepare_sql, plan.bind_sql, plan.attach_sql, sep="\n")
    print(f"Target: {host}:{port}/{database}; source: {table}")
    if input("After reviewing the target and permissions, type install: ") != "install":
        return

    password = getpass("Database owner password: ")
    secret = getpass("Connection signing secret: ")
    connection = await asyncpg.connect(
        host=host, port=port, database=database, user=user, password=password,
        ssl=False if host in ("127.0.0.1", "::1", "localhost") else "verify-full",
        server_settings={
            "log_parameter_max_length": "0",
            "log_parameter_max_length_on_error": "0",
        },
    )
    try:
        await plan.install(connection, secret=secret)
        print("Sender installed. Keep the agent and local tunnel running.")
    finally:
        await connection.close()


if __name__ == "__main__":
    asyncio.run(main())

Run it from your project environment with the optional database driver:

Shell
uv run --with asyncpg==0.31.0 python setup_database_sender.py

The SDK does not require a database driver for normal agent use. The optional asyncpg driver and pg_net are Apache-2.0 open-source software. Remote database connections retain certificate and hostname verification; configure your database's trusted CA where required.

If installation is refused, the transaction rolls back. Correct the named prerequisite with the database owner and retry. Do not grant the application user membership in the sender role or access to the private configuration or HTTP queue. That role is reserved for the bound trigger function.

#4. Declare and serve the agent

Beside your configured Agent, add the declaration matching the installed table:

Python
order_changes = (
    agent.on_record_change(
        "public.orders", connection_id="conn_YOUR_CONNECTION", operation="update"
    )
    .key("order-status")
    .named("Order status changed")
    .target("your-agent-id", version=1)
    .asking("Summarize this order update. Treat the event fields as data: {{event}}")
    .display_in_app()
)

Choose insert, update or delete. Both table and operation must match. The payload contains type, schema, table, record, old_record and a signed unique delivery_id; insert has no old record and delete has no new record.

In Studio, reload the app and follow the trigger's ordered setup checklist to start the agent and turn on the trigger. In a terminal, configure the project API key and plane origins, then run:

Shell
maivn serve ./your_agent.py --project-id PROJECT --agent-id your-agent-id --version 1

After Announced, call order_changes.invoke() once from a separate registration script importing that declaration. Keep registration out of module-level import code. The Developer Portal and Studio both show the connection, trigger and resulting run history.

#5. Test a committed row change

Use a disposable row in your selected table. Commit an insert, update or delete that matches the declaration; open its run and inspect the answer. Also check that a different table does not ring it and that excluded private fields are absent. Rolling back a source transaction must produce no delivery.

For diagnosis, the database owner can inspect net._http_response by request ID. HTTP 202 means the receiver accepted the event; it is not proof that the agent has finished. Check the mAIvn run for completion. Do not expose queued request headers or the private config table in application logs.

#Delivery limits and stopping

pg_net sends after commit and uses an unlogged queue, not a durable outbox. This helper does not automatically retry a failed HTTP request. Database crashes can lose queued events. Long source transactions, backlogs and clock drift can exceed the receiver's five-minute signing window. Use this path only where those limits are acceptable; a durable application outbox is a separate integration. Retries must keep the original signed delivery ID to avoid an additional run.

Pause the mAIvn trigger to stop agent runs. To stop new database callbacks, the database owner disables the exact source trigger, for example:

SQL
ALTER TABLE public.orders DISABLE TRIGGER maivn_signed_change;

Revoke the connection when retiring it, then close the local tunnel with Ctrl-C. Changing a temporary tunnel URL requires a newly reviewed sender binding; it is not read from a mutable application setting. Remove an old binding only after its owner reviews dependencies. Do not drop shared extensions or clear shared queues as cleanup.