Cron Jobs
Visão Geral
Seção intitulada “Visão Geral”O Rebase inclui um agendador de cron jobs integrado para executar tarefas em segundo plano recorrentes — limpeza de dados, geração de relatórios, verificações de integridade (health checks), sincronizações com APIs externas e muito mais.
Os cron jobs seguem o mesmo padrão de descoberta baseada em arquivos das custom functions: adicione um arquivo TypeScript no diretório crons/, e o Rebase o registrará e agendará automaticamente.
- Zero dependências — Nenhuma biblioteca externa de agendamento necessária
- Admin API — Endpoints REST para listar, disparar, habilitar/desabilitar e visualizar logs
- Painel do Studio — Monitore todos os jobs, veja o histórico de execuções e dispare execuções manualmente
- Persistência no banco de dados — Logs de execução armazenados no PostgreSQL, sobrevivendo a reinicializações
- Cache em memória — Ring buffer rápido (últimas 50 execuções) para o painel, suportado pelo banco de dados
Definindo um Cron Job
Seção intitulada “Definindo um Cron Job”Crie um arquivo no seu diretório backend/crons/ que faça a exportação padrão (export default) de uma definição de cron. Utilize o helper defineCron de @rebasepro/server para inferência de tipos e autocompletar:
// backend/crons/health-check.tsimport { defineCron } from "@rebasepro/server";
export default defineCron({ schedule: "*/5 * * * *", // every 5 minutes name: "System Health Check", description: "Monitors uptime and memory usage",
async handler(ctx) { ctx.log("Running health check...");
const uptime = process.uptime(); const mem = process.memoryUsage();
ctx.log(`Uptime: ${Math.round(uptime)}s`); ctx.log(`Heap: ${Math.round(mem.heapUsed / 1024 / 1024)}MB`);
return { uptimeSeconds: Math.round(uptime), heapUsedMB: Math.round(mem.heapUsed / 1024 / 1024), }; },});O rebase dev observa o diretório de crons, portanto, um job adicionado enquanto ele está em execução é registrado na próxima recarga — sem necessidade de reinicialização. (Ele precisa ser informado: o diretório é escaneado em vez de importado, logo o observador não pode inferi-lo.)
O nome do arquivo (sem a extensão) torna-se o ID exclusivo do job — por exemplo, health-check.
Configuração
Seção intitulada “Configuração”Habilite os cron jobs adicionando cronsDir à configuração do seu backend:
const instance = await initializeRebaseBackend({ // ... other config functionsDir: path.resolve(__dirname, "../functions"), cronsDir: path.resolve(__dirname, "../crons"), // ← add this});Isso é tudo. O Rebase irá:
- Escanear o diretório em busca de arquivos
.ts/.js - Registrar cada exportação padrão como um cron job
- Criar automaticamente as tabelas
rebase.cron_logs,rebase.cron_claimserebase.cron_job_stateno PostgreSQL (se o driver suportar SQL) - Iniciar o agendador e preencher os contadores a partir dos logs existentes no banco de dados
- Disponibilizar as rotas REST de administração em
/api/admin/cron
Cron jobs como valores
Seção intitulada “Cron jobs como valores”Desde 0.25 Na 0.24 os cron jobs vêm apenas de um diretório.
crons recebe os jobs como um record do ID do job para a definição, ao lado de collections. Um diretório é uma das formas de montá-lo: loadCrons(dir) lê um diretório exatamente nesse formato.
import path from "path";import { defineCron, initializeRebaseBackend, loadCrons } from "@rebasepro/server";
const nightlyReport = defineCron({ schedule: "0 3 * * *", async handler({ log }) { log("building the report"); }});
await initializeRebaseBackend({ app, server, crons: { "nightly-report": nightlyReport } });// the same as cronsDir:await initializeRebaseBackend({ app, server, crons: await loadCrons(path.resolve(__dirname, "../crons")) });Dê crons ou cronsDir, não ambos: a inicialização recusa o par. Construir o backend registra os jobs; o que inicia os timers deles é start(), que o initializeRebaseBackend chama para você. Com createRebaseBackend você mesmo o chama, no processo que deve executá-los. loadCronJobsFromDirectory e loadCronJobsWithDiagnostics continuam funcionando, estão obsoletas e avisam uma única vez.
Sintaxe do Agendamento
Seção intitulada “Sintaxe do Agendamento”As expressões Cron utilizam o formato padrão de 5 campos:
┌───────────── minute (0–59)│ ┌─────────── hour (0–23)│ │ ┌───────── day of month (1–31)│ │ │ ┌─────── month (1–12)│ │ │ │ ┌───── day of week (0–6, Sunday = 0)│ │ │ │ │* * * * *| Expressão | Significado |
|---|---|
* * * * * |
A cada minuto |
0 * * * * |
A cada hora |
0 3 * * * |
Diariamente às 3:00 |
0 0 * * 1 |
Toda segunda-feira à meia-noite |
0 9 1 * * |
Primeiro dia de cada mês às 9:00 |
0,30 * * * * |
A cada 30 minutos (nos minutos :00 e :30) |
0 9-17 * * 1-5 |
A cada hora, das 9:00 às 17:00, apenas em dias úteis |
Valores de intervalo (*/n), faixas (a-b) e listas (a,b,c) são todos suportados.
Um agendamento é comparado com o horário local do seu fuso. Numa mudança de horário de verão, isso significa que um job de horário fixo dentro da hora repetida (30 2 * * * onde os relógios atrasam às 03:00) é executado nas duas passagens dessa hora, e um dentro da hora pulada, quando os relógios adiantam, não é executado naquele dia.
Referência de CronJobDefinition
Seção intitulada “Referência de CronJobDefinition”timezone é novo — na versão 0.17.3, o agendamento é sempre interpretado no fuso horário do próprio host. Tudo o mais nesta interface já está disponível.
interface CronJobDefinition { // Cron schedule expression (5-field format) schedule: string;
// IANA zone the schedule is read in, e.g. "Europe/Madrid". Without it the // schedule is read in the host's own zone — UTC in nearly every container, // yours on a laptop — so name it. An unknown zone is refused when the job // loads rather than read as local time. timezone?: string;
// Human-readable name shown in Studio name: string;
// Optional description shown in Studio description?: string;
// Whether the job runs (default: true). A pause or a resume from Studio // or the admin API overrides it, for every process, until reset. enabled?: boolean;
// Max execution time in seconds (default: 300). Infinity means no // timeout; 0, a negative number or NaN is refused when the job loads. timeoutSeconds?: number;
// How far back to look on startup for a slot that elapsed while no // instance was ticking (default: off). See "Cron across instances". catchUpWindowSeconds?: number;
// The function to run on each tick handler: (ctx: CronJobContext) => Promise<unknown> | unknown;}Contexto do Handler
Seção intitulada “Contexto do Handler”Cada handler recebe um CronJobContext contendo métodos utilitários e a instância do Rebase Client:
interface CronJobContext { // The job's unique ID (derived from filename) jobId: string;
// The scheduled tick timestamp scheduledAt: Date;
// Logger — lines appear in Studio, the logs API and the server log log: (...args: unknown[]) => void;
// Aborted when the run exceeds `timeoutSeconds`, or when a shutdown's // wait for it runs out signal: AbortSignal;
// The server-side Rebase singleton — the same object `import { rebase } // from "@rebasepro/server"` returns, and the same one `defineFunction` // hands its callback. rebase: RebaseServerClient;}Use ctx.log() para gerar saídas estruturadas. Essas linhas são capturadas no log de execução e ficam visíveis no Studio e por meio da REST API. Cada linha também é gravada no log do servidor no nível info, com um campo cron contendo o ID do job, então ela aparece em qualquer lugar que colete a saída do seu contêiner, inclusive nos logs de depuração do Rebase Cloud. Essa cópia é mascarada como as próprias linhas de log do servidor: os valores de chaves como password ou apiKey, e o SQL e os parâmetros de uma consulta que falhou, são substituídos. O log de execução armazena a linha sem esse mascaramento.
Desde 0.25 O log de execução guarda as primeiras 500 linhas de uma execução ou 64 KB delas, o que vier primeiro, e depois uma linha dizendo quantas houve além disso: … 9500 more lines not kept — see the server log. O log do servidor recebe todas as linhas. Um job que registra uma linha por item processado deve esperar que seu histórico mostre o início da execução.
ctx.signal — interrompa o trabalho quando a execução parar
Seção intitulada “ctx.signal — interrompa o trabalho quando a execução parar”O timeout encerra a execução: o agendador para de esperar e registra uma falha. Ele não encerra o handler. Passe ctx.signal para qualquer chamada que o aceite, e o trabalho será interrompido junto com ele:
export default defineCron({ name: "Sync inventory", schedule: "*/15 * * * *", timeoutSeconds: 60, async handler({ signal, log }) { const res = await fetch("https://supplier.example.com/stock", { signal }); log(`fetched ${res.status}`); }});Sem isso, um job cujo timeout coincida com seu intervalo deixa vazar uma requisição abandonada por tick — o que é invisível, já que cada execução já foi registrada como falha.
Interagindo com o banco de dados e serviços via ctx.rebase
Seção intitulada “Interagindo com o banco de dados e serviços via ctx.rebase”ctx.rebase.dataAsAdmin é o plano de dados com escopo administrativo. Um cron não possui um usuário por requisição, portanto não há alternativa com escopo de usuário aqui — você mesmo deve definir os filtros de cada consulta.
// backend/crons/expire-users.tsimport { defineCron } from "@rebasepro/server";
export default defineCron({ schedule: "0 0 * * *", // Daily at midnight name: "Expire Inactive Accounts",
async handler(ctx) { ctx.log("Checking for expired trial users...");
// Fetch using the pre-initialized data driver. `collection<Row>(slug)` // gives the query builder the row type — `where` keys are checked // against it. Every filter is an `[operator, value]` tuple; a bare // value is passed straight through and builds a malformed query. const users = ctx.rebase.dataAsAdmin.collection<{ id: string; email: string; trial_status: string; trial_ends_at: string; status: string; }>("users");
const { data: trials } = await users.find({ where: { trial_status: ["==", "active"], trial_ends_at: ["<", new Date().toISOString()] } });
ctx.log(`Found ${trials.length} users with expired trials.`);
for (const user of trials) { await users.update(user.id, { trial_status: "expired", status: "disabled" });
// Send email notification using the Rebase email service await ctx.rebase.email.send({ to: user.email, subject: "Your trial has expired", html: "<p>Please upgrade your subscription to continue.</p>" }); } }});REST API
Seção intitulada “REST API”Listar, disparar, pausar e retomar jobs por HTTP, e o client.cron do SDK tipado: API de Cron Jobs.
Painel do Studio
Seção intitulada “Painel do Studio”Quando os cron jobs estão configurados, uma ferramenta Cron Jobs aparece no Rebase Studio sob Compute, ao lado do console JS. O painel disponibiliza:
- Lista de jobs — Todos os jobs registrados com indicadores de status em tempo real
- Painel de detalhes — Agendamento, próxima/última execução, duração e detalhes de erro
- Histórico de execuções — Entradas de log expansíveis com saídas e resultados capturados
- Disparo manual — Execute qualquer job sob demanda com um único clique. O painel aguarda o fim da execução e diz como ela foi: uma execução que falhou mostra o seu erro
- Habilitar/desabilitar — Pause e retome jobs sem reiniciar o servidor, em todos os processos de uma vez; uma pausa se mantém após reinicializações e deploys
- Jobs que não estão lá — cada job que o agendador recusou, com o seu agendamento e o motivo, e quantos arquivos de cron falharam ao carregar. Nenhum deles está agendado; veja Jobs Que Não Estão Lá
O painel é atualizado automaticamente a cada 15 segundos.
O painel mostra a mesma coisa, seja qual for o processo que o serve. Um job que
outro processo está executando aparece como em execução.
Em todo processo que tem um armazenamento persistente, o número de execuções, o
número de falhas e a última execução são lidos do banco de dados, onde as execuções de todos os processos são contadas. Os contadores do próprio processo guardam apenas as
execuções que ele fez: o papel api ao lado de um worker não executa nada, e, de duas
réplicas que agendam, nenhuma vê os slots que a outra reivindicou. Só quando
o banco de dados não pode ser lido o card recorre aos contadores do próprio processo, com um
aviso no log do servidor.
Desde 0.25 As contagens são de toda a vida; o histórico são os últimos 30 dias. A linha de cada execução em rebase.cron_logs é excluída 30 dias depois de iniciada, por uma varredura a cada hora, enquanto as contagens de execuções e de falhas continuam a contá-la. Uma execução ignorada conta como execução, como sempre contou: é uma linha do histórico.
Validação de Agendamento e Análise AST
Seção intitulada “Validação de Agendamento e Análise AST”Na inicialização do backend, o Rebase analisa todos os agendamentos de cron registrados usando um expansor de cron em JS com zero dependências:
- Verificação de Sintaxe: Garante que a string contenha exatamente 5 campos separados por espaços (
minuto,hora,dia do mês,mês,dia da semana). - Expansão de Intervalos: Decompõe passos (
*/15), faixas (9-17) e listas separadas por vírgulas (0,30) em arrays explícitos de inteiros válidos mapeados para seus respectivos limites (ex.: minutos0-59, horas0-23, meses1-12). - Se alguma expressão de cron falhar na validação, o Rebase rejeita a definição, grava um erro de inicialização no log e recusa registrar o job para evitar falhas durante o tempo de execução.
Nos Bastidores: Correção de Desvio de Relógio (Clock-Drift)
Seção intitulada “Nos Bastidores: Correção de Desvio de Relógio (Clock-Drift)”Agendadores padrão baseados em intervalos (como setInterval) sofrem desvio ao longo do tempo e causam picos significativos de CPU devido a atrasos de agendamento no event loop do sistema operacional. Para garantir a precisão da execução, o Rebase implementa um loop dinâmico de cálculo do tempo-alvo:
- Cálculo do Candidato: Ao concluir um job ou iniciar o agendador, o Rebase calcula o timestamp exato do próximo minuto candidato correspondente.
- Espera Dinâmica: Ele calcula a diferença em milissegundos (
nextRun.getTime() - now.getTime()) e agenda um únicosetTimeout. - Limite de Segurança contra Desvio: Um buffer mínimo de espera (
MIN_SCHEDULE_INTERVAL_MS) de 5.000ms é imposto. Se um tick do agendador for concluído com extrema rapidez, esse limite evita disparos duplos quase instantâneos. - Encerramento Amigável: Os timers são explicitamente desacoplados do event loop do Node.js usando
timer.unref(), garantindo que agendadores de cron em segundo plano não bloqueiem o encerramento limpo de processos durante deploys.
Mais de um processo
Seção intitulada “Mais de um processo”Todo processo cujo agendador está ligado arma os mesmos timers; o banco de dados decide qual deles executa cada slot. Como um slot roda uma única vez, como um slot perdido em uma reinicialização é recuperado, como uma pausa chega a todas as réplicas e por que um disparo manual nunca roda ao lado de uma execução agendada está em Cron entre instâncias.
Timeouts e Isolamento de Erros
Seção intitulada “Timeouts e Isolamento de Erros”- Corrida de Timeout Forçada: Os blocos de execução são encapsulados em um
Promise.racecontra um timer de timeout baseado emtimeoutSeconds(padrão:300segundos / 5 minutos;Infinitypara nenhum timeout). Se o handler travar além desse limite,ctx.signalé abortado e a promise é rejeitada, disparando:Error: Cron job "<id>" timed out after <N>msA ação de abortar é a parte que realmente interrompe o trabalho; a rejeição apenas faz o agendador parar de esperar. Um handler que ignoractx.signalcontinua sendo executado além de sua própria execução. - Encerramento:
backend.shutdown()aguarda uma execução em andamento, dentro do mesmo orçamento da fila de jobs (dois terços do timeout de encerramento). Uma execução que ainda esteja rodando quando ele se esgota temctx.signalabortado e é registrada como falha, com o motivo. Seu slot continua reivindicado, então nenhuma outra instância a executa de novo. - Try/Catch com Proteção contra Falhas: Cada handler de job roda dentro de um wrapper isolado. Quaisquer exceções não tratadas são interceptadas, formatando o rastreamento do erro (traceback) em uma string, alterando o status do job para
"error"e atualizando o contador de falhas do job. Uma pane dentro de uma tarefa de cron específica nunca derrubará o loop do agendador ou o servidor web HTTP Hono principal. - Ring Buffer em Memória: O agendador mantém um ring buffer contendo as últimas 50 execuções de cada job. Esse buffer permanece em memória para viabilizar leituras quase instantâneas a partir do Rebase Studio.
Esquema de Persistência no Banco de Dados
Seção intitulada “Esquema de Persistência no Banco de Dados”Quando adaptadores de banco de dados compatíveis com SQL (por exemplo, PostgreSQL) estão ativos, o Rebase provisiona a tabela rebase.cron_logs:
CREATE SCHEMA IF NOT EXISTS rebase;
CREATE TABLE IF NOT EXISTS rebase.cron_logs ( id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text, job_id TEXT NOT NULL, started_at TIMESTAMPTZ NOT NULL, finished_at TIMESTAMPTZ NOT NULL, duration_ms INTEGER NOT NULL, success BOOLEAN NOT NULL DEFAULT true, error TEXT, -- Stack trace or error message result JSONB, -- Return value of handler logs JSONB, -- Ring buffer array of ctx.log outputs manual BOOLEAN NOT NULL DEFAULT false -- True if triggered from Studio/REST);
ALTER TABLE rebase.cron_logs ADD COLUMN IF NOT EXISTS counted BOOLEAN; -- the run is in the counters below
CREATE INDEX IF NOT EXISTS idx_cron_logs_job ON rebase.cron_logs(job_id, started_at DESC);CREATE INDEX IF NOT EXISTS idx_cron_logs_started ON rebase.cron_logs(started_at);CREATE INDEX IF NOT EXISTS idx_cron_logs_uncounted ON rebase.cron_logs(job_id, started_at DESC) WHERE counted IS NULL;
CREATE TABLE IF NOT EXISTS rebase.cron_job_state ( job_id TEXT PRIMARY KEY, enabled BOOLEAN, -- NULL: follow the job's own `enabled` updated_at TIMESTAMPTZ, -- when `enabled` was last set, and by whom updated_by TEXT, running_until TIMESTAMPTZ, -- the run lease: live while in the future running_by TEXT, total_runs BIGINT NOT NULL DEFAULT 0, -- all-time, skips included total_failures BIGINT NOT NULL DEFAULT 0, last_run_at TIMESTAMPTZ, -- the most recently started run last_duration_ms INTEGER, last_success BOOLEAN, last_error TEXT);Cada uma é criada na inicialização se estiver faltando, então um banco de dados
anterior a ela a recebe no próximo deploy. Nenhuma delas é uma coleção, e o papel
de usuário final rebase_user não tem nenhum privilégio sobre elas: um
cron_job_state gravável permitiria que um usuário autenticado pausasse um job
para todos, ou segurasse seu lease para que nada o execute.
Desde 0.25 A linha de uma execução e sua contagem são gravadas em uma única instrução: o insert em cron_logs e um upsert dos contadores do job em cron_job_state. Uma listagem lê uma linha de cron_job_state por job, por mais longo que seja o histórico, e o agendador também, ao preencher totalRuns e totalFailures na inicialização. As gravações de log não seguram a execução; se uma falhar, o agendador registra o erro e segue em frente, com a execução ainda no ring buffer em memória.
A cada hora, e quando inicia, o agendador exclui as linhas do histórico com mais de 30 dias e conta as execuções ainda não contadas, ambos em lotes de 5.000 linhas. Um histórico gravado antes da 0.24 é contado pela primeira dessas varreduras, fora do caminho de inicialização. Até ela terminar, uma listagem soma aos contadores as execuções que ainda não contou, de modo que os números não mudam na atualização. O mesmo vale para uma execução que um servidor mais antigo registra durante um deploy gradual.
Exemplo: Job Diário de Limpeza
Seção intitulada “Exemplo: Job Diário de Limpeza”// backend/crons/cleanup-sessions.tsimport type { CronJobDefinition } from "@rebasepro/types";import { rebase } from "@rebasepro/server";
const job: CronJobDefinition = { schedule: "0 3 * * *", // daily at 3 AM name: "Cleanup Expired Sessions", description: "Removes user sessions older than 30 days",
async handler(ctx) { ctx.log("Starting session cleanup...");
// Admin-scoped data access — see `ctx.rebase` above. const cutoff = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(); const expired = await ctx.rebase.dataAsAdmin.sessions.findAll({ where: { last_seen_at: ["<", cutoff] } }); for (const session of expired) { await ctx.rebase.dataAsAdmin.sessions.delete(session.id as string); }
ctx.log(`Cleaned up ${expired.length} expired sessions`);
return { deletedSessions: expired.length }; },};
export default job;Crons no Grafo de Recursos
Seção intitulada “Crons no Grafo de Recursos”Cada arquivo de cron é também uma declaração. O comando rebase resources o lista sob o nome do arquivo — o mesmo ID usado pelo agendador para executá-lo e exibido no Studio — junto com seu agendamento e fuso horário, permitindo que o host conheça os cronogramas de um projeto antes de executar qualquer rotina. Um cron não se vincula a nenhuma variável de ambiente; o rebase status o exibe em verde sem necessidade de configuração adicional.
Ler o agendamento requer importar o arquivo, e o rebase resources é uma etapa de build: sem .env, sem segredos. Portanto, mantenha o escopo do módulo de um cron livre de qualquer código que leia configurações no momento da importação — como um cliente de banco de dados instanciado no topo de um helper ou um env.ts que valida DATABASE_URL. Em vez disso, importe esse tipo de operação dentro do handler:
async handler({ log }) { const { runSeed } = await import("../src/seed.js"); await runSeed(); log("done");}O handler é executado na instância de implantação, onde essas variáveis de fato existem. Um import no nível raiz do mesmo módulo tornaria o grafo derivável apenas em uma máquina que por acaso possuísse um .env — além de carregar todas as dependências a cada inicialização que simplesmente registre o job.
Desde 0.25 Carregar um cron
não declara nada. Uma etapa de build sua que queira os crons no grafo diz isso
com declareCrons:
import { declareCrons, loadCrons } from "@rebasepro/server";
declareCrons(await loadCrons("./crons", { onSkip: (problem) => console.warn(problem) }));Próximos Passos
Seção intitulada “Próximos Passos”- Visão Geral do Backend — Referência completa de configuração do backend
- Callbacks de Entidades — Execute lógicas baseadas em alterações de dados
- Integração com Webhooks — Envie notificações a partir de eventos
- Cron entre instâncias — Uma execução por slot, slots perdidos, pausa em todo lugar e execuções sobrepostas