14. Scheduled Jobs

Alex needs to clean up expired sessions and send weekly reports automatically.

Scheduled jobs let you run SQL statements or database functions on a recurring schedule. No server needed — Eurobase executes them automatically in your project's database.

Creating a scheduled job

  1. Go to the Cron tab in your project
  2. Click New Job
  3. Give it a name (e.g. "Clean expired sessions")
  4. Choose a schedule preset or write a custom cron expression
  5. Select the action type: SQL (run a query) or RPC (call a function)
  6. Write the SQL or function name
  7. Click Create

Common examples

Clean up expired sessions (every hour)

Schedule: 0 * * * *

DELETE FROM sessions WHERE expires_at < now()

Send weekly digest (every Monday at 9am)

Schedule: 0 9 * * 1

SELECT send_weekly_digest()

Archive old records (daily at midnight)

Schedule: 0 0 * * *

INSERT INTO archive SELECT * FROM logs WHERE created_at < now() - interval '30 days'; DELETE FROM logs WHERE created_at < now() - interval '30 days';

Check pending orders (every 5 minutes)

Schedule: */5 * * * *

SELECT process_pending_orders()

Cron schedule reference

FieldValuesSpecial
Minute0-59* , */N
Hour0-23* , */N
Day of month1-31* , */N
Month1-12* , */N
Day of week0-6 (Sun=0)* , */N

Each field is *, */N, a number, or a comma list of these. Ranges (1-5) and names (MON) aren't supported — list the values instead: 0 9 * * 1,2,3,4,5 is weekdays at 9:00.

Timezone: a job's schedule is read in its timezone, UTC unless you pick another one in the form (e.g. Europe/Stockholm, which follows daylight saving time). The form shows the next run times before you save, and the job list shows each job's next run. In zones with daylight saving time, a time in the hour that repeats in autumn runs twice, and a time in the hour skipped in spring doesn't run that day. Through the API, set timezone on the job.

When both day of month and day of week are set, a job runs only on days that match both (standard cron runs on either): 0 9 13 * 5 runs on Friday the 13th only.

Quick reference

* * * * * — every minute */5 * * * * — every 5 minutes 0 * * * * — every hour 0 0 * * * — daily at midnight 0 9 * * 1 — Monday 9am 0 0 1 * * — 1st of month

Plan limits: Free plan includes 2 scheduled jobs. Pro plan has unlimited jobs.

Jobs run in your project's own schema, as your project's database role — they can't reach other schemas. One statement per job, run in its own transaction: everything it does, including what the functions it calls change, commits together or not at all. Each run may take up to 30 seconds.

Not allowed in a SQL job: DO blocks, CREATE FUNCTION / CREATE PROCEDURE and temporary objects (all allowed in the SQL editor), plus what the SQL editor refuses too: set_config(), role and privilege statements, and references to other schemas or the system catalogs. The job is refused when you save it. For multi-step logic, create a function in the SQL editor and call it from the job, e.g. SELECT my_function();

Test Run runs the statement exactly like a scheduled run (same role, same Run as service role setting, same checks), then always rolls back — including changes made by the functions it calls. Nothing is committed (sequence values still advance). A Test Run may take up to 25 seconds. The console shows the Test Run button for SQL jobs; to test a function, use a SQL job with SELECT my_function();

Run as service role (default for new jobs): row-level security policies see the job as the service role, like an edge function called without a user, so policies that allow the service role apply and housekeeping jobs (purging expired rows, rollups) can reach rows across users. Custom policies that don't check for the service role still apply as written. With it off, the job has no user context and only sees rows your policies allow to everyone. Jobs created before this option existed have it off; switch it on in the job's settings.

Running a job once

Jobs always repeat on their schedule; there is no "run once" option. For a one-time task (seeding data, a backfill):

  1. Put the work in a function and make it safe to run twice — e.g. return early if the data already exists.
  2. Create a SQL job with SELECT my_function(); and check it with Test Run (rolled back).
  3. Save the job with a schedule a few minutes ahead — the form shows when it will next run.
  4. Check the job's run history, then delete or disable the job.

RPC Functions

RPC (Remote Procedure Call) functions are reusable PostgreSQL functions stored in your database. Unlike raw SQL cron actions, functions can contain complex logic (loops, conditionals, error handling) and can be called from both cron jobs and your app via the SDK.

RPC vs Cron Job vs DB Trigger vs Edge Function: Eurobase has four kinds of "server-side code" and the distinction matters. Quick gist: RPC = callable SQL (this section). Cron Job = scheduled SQL (above). DB Trigger = reactive SQL fired by row events on a table (managed in Database → Triggers). Edge Function = TypeScript in a Deno container, for external API calls and JS-ecosystem things. The full comparison table in the Edge Functions chapter has language, transactional semantics, and use cases side by side.

Creating a function

When creating a cron job, select "RPC Function" and click "Create New Function". Choose a name, language, return type, and write the function body.

Example: Delete old notifications (void — for cron)

Language: PL/pgSQL · Returns: void

BEGIN
  DELETE FROM notifications WHERE created_at < now() - interval '30 days';
END;

Expired sessions and email tokens are cleaned up by the platform; your functions can't read or change those tables.

Example: Get active user count (integer — for SDK)

Language: SQL · Returns: integer

SELECT count(*)::integer FROM users WHERE last_sign_in_at > now() - interval '30 days';

Example: Generate daily stats (jsonb — for SDK)

Language: PL/pgSQL · Returns: jsonb

DECLARE result jsonb;
BEGIN
  SELECT jsonb_build_object(
    'total_users', (SELECT count(*) FROM users),
    'active_today', (SELECT count(*) FROM users WHERE last_sign_in_at > now() - interval '1 day')
  ) INTO result;
  RETURN result;
END;

Return types explained

TypeWhen to useSDK result
voidCron jobs, cleanup tasks, side effects onlynull
textReturn a message or formatted string"hello world"
integerReturn a count or numeric value42
booleanReturn true/false checkstrue
jsonbReturn structured data (objects, arrays){'key': 'value'}

Viewing, editing and exporting a function

Click a function under Cron & RPC → Functions to see its full CREATE OR REPLACE FUNCTION statement. Open in SQL editor puts it in a new editor tab: change it there and run it to update the function in place — nothing needs to be dropped. Functions you created with SQL show up here too, with their arguments. One exception: a function marked security definer can't be recreated as one from your SQL (SECURITY DEFINER isn't accepted), so ask support before changing it.

  • CLI: eurobase functions get <name> > <name>.sql saves the definition; --args "team_id uuid" picks one of several functions with the same name.
  • API: GET /platform/projects/$PROJECT_ID/schema/functions/<name>?args=… (Developer role) returns the function with its definition; without args, a name with several overloads answers 409 with the candidates.
  • MCP: listDbFunctions and getDbFunction.
  • Trigger functions and platform helpers: these lookups also return your trigger functions (their names: GET .../schema/functions/triggers, or a table’s triggers under Database) and the helpers the platform installs in every project, auth_uid(), auth_role() and auth_email(), marked "platform": true. The helpers are managed by the platform and are the same in every project; don’t change them from SQL, because your RLS policies depend on them.
  • All at once: a full project export (Compliance → Export) includes schema/schema.sql with every function.

Reading pg_proc, pg_get_functiondef() or information_schema from your own SQL is refused (the system catalogs aren't scoped to your project), so these are the ways to get a definition.

Calling functions from the SDK

Functions with a return type (not void) can be called from your app. The return value is sent back as JSON.

// Call an RPC function from the SDK
const { data, error } = await eb.db.rpc('get_active_user_count')
console.log(data) // 42

// Call a function that returns JSON
const { data: stats } = await eb.db.rpc('generate_daily_stats')
console.log(stats) // { total_users: 150, active_today: 23 }

Anyone with your public key can call any function. POST /v1/db/rpc/<name> doesn't check which key was used, and who may execute a function can't be restricted with GRANT / REVOKE. Policies still apply to the tables a function reads and writes, but a function meant only for your server (credits, payouts, admin fixes) must check the caller itself:

IF NOT public.is_service_role() THEN
  RAISE EXCEPTION 'server only';
END IF;

For a function signed-in users may call, check auth_uid() instead; it only has a value when the call carries a user token.

Cron + SDK tip: Create a function that returns void for cron (e.g. cleanup tasks), and separate functions that return data for your SDK calls (e.g. stats, reports). A function can do both — perform side effects and return a result.