Scheduled Jobs API
A API de Scheduled Jobs cria um projeto que executa uma rotina em horários definidos, atualiza o cron e consulta o histórico de execuções, logs e métricas por execução. Todas as requisições usam a URL base https://api.zenifra.com/v1.
Autenticação e permissões
Use uma API Key da organização ou um token de usuário aceito pela sua integração. Inclua o contexto da organização em x-organization-id e envie Content-Type: application/json nas requisições com corpo.
x-api-key: sua-api-key
x-organization-id: sua-organization-id
Content-Type: application/jsonOs scopes mínimos são:
| Operação | Scope | Recurso |
|---|---|---|
| Consultar planos | acesso à API de planos | — |
| Criar um Job Docker/OCI ou GitHub | project.create | organization:* |
| Atualizar o cron | project.schedule.update | project:<project-id> |
| Listar execuções | project.read | project:<project-id> |
| Consultar o custo do ciclo | project.billing.read | project:<project-id> |
| Cancelar uma execução ativa | project.job-run.cancel | project:<project-id> |
| Consultar logs da execução ou builds GitHub | project.logs.read | project:<project-id> |
| Consultar métricas da execução | project.metrics.read | project:<project-id> |
A organização e o projeto precisam pertencer ao contexto autorizado. Uma resposta 403 indica que a credencial não tem o scope ou recurso necessários. Para uma fonte GitHub, a conta GitHub também precisa estar conectada à organização.
Consultar planos e preços
Consulte o catálogo antes de criar ou apresentar uma estimativa:
GET /v1/project/job/plansA resposta usa o catálogo de produto dedicado a Jobs. Os IDs têm o prefixo job-, como job-basic, job-premium, job-premium_plus e job-business. Leia sempre price_per_minute, currency, payment_mode, features e permissions da resposta. price_per_minute é um número em centavos de BRL, pode ser fracionário, e payment_mode é per_minute; divida o preço por 100 para exibir reais. O catálogo é a fonte da tarifa vigente; não fixe preços em integrações.
{
"status": "success",
"data": [
{
"plan": "job-basic",
"price_per_minute": 2,
"currency": "brl",
"payment_mode": "per_minute",
"features": ["Execuções de até 60 minutos"],
"permissions": {
"free_storage_size": "1"
}
}
]
}Se Jobs estiver desabilitado, este endpoint retorna 503 e code: SCHEDULED_JOBS_UNAVAILABLE. Trate esse caso como indisponibilidade específica do catálogo de Jobs; outros erros continuam sendo falhas da consulta.
Criar um Job
Crie o projeto com POST /v1/project, usando um plano job-* e payment_mode: per_minute. O corpo deve fornecer exatamente uma fonte: image para uma imagem Docker/OCI pronta ou github para um repositório conectado.
Fonte Docker/OCI
Este exemplo usa uma imagem pública, armazenamento efêmero e um processo batch explícito:
POST /v1/project
Idempotency-Key: chave-unica-com-ao-menos-16-caracteres{
"name": "relatorio-noturno",
"description": "Gera o relatorio diario",
"plan": "job-basic",
"payment_mode": "per_minute",
"config": {
"type_project": "job",
"image": {
"url": "docker.io/acme/relatorio:1.0",
"is_public": true
},
"envs": [
{ "name": "REPORT_FORMAT", "value": "csv" }
],
"storage": {
"persistent": false,
"capacity": 1
},
"job": {
"cron": "0 3 * * *",
"command": ["/app/report"],
"args": ["--daily"]
}
}
}cron tem exatamente cinco campos e é interpretado em UTC. command e args são listas de strings; ambos são opcionais na API. Quando os dois são omitidos, a imagem usa seu próprio entrypoint e CMD. Um Job não aceita porta, domínio, exposição, instâncias ou auto-scaling. O fluxo CLI V1 aceita somente config.image e não oferece config.github, job.command ou job.args; use esta API para o fluxo avançado.
Fonte GitHub
Use uma conta GitHub conectada e informe o repositório e a branch. O build cria o artefato que será executado pelo Job; start_command, pre_build_command e build_command pertencem ao fluxo de build da origem, enquanto job.command e job.args controlam o processo batch quando definidos.
{
"name": "sincronizacao-github",
"description": "Sincroniza dados a cada hora",
"plan": "job-basic",
"payment_mode": "per_minute",
"config": {
"type_project": "job",
"github": {
"repository_owner": "acme",
"repository_name": "data-jobs",
"branch": "main",
"auto_deploy": false,
"runtime": "nodejs",
"version": "24",
"start_command": "node worker.js",
"pre_build_command": null,
"build_command": "npm ci"
},
"envs": [
{ "name": "MODE", "value": "production" }
],
"storage": {
"persistent": true,
"capacity": 5,
"dir_path_to_persist": "/data"
},
"job": {
"cron": "0 * * * *",
"command": ["node"],
"args": ["worker.js"]
}
}
}Consulte o build em GET /v1/project/:id/github/builds e os logs em GET /v1/project/:id/github/builds/:buildId/logs. Esses endpoints usam project.logs.read e têm histórico próprio; não confunda um build failed com uma execução de Job failed.
Contrato de execução
O código de saída pertence ao processo da imagem:
- código
0resulta emsucceeded; - qualquer código não zero resulta em
failed;1é somente um exemplo; - sem
commandeargs, o entrypoint e oCMDda imagem são usados; - um processo que não termina permanece
runningaté cancelamento ou até o deadline de 60 minutos, quando viradeadline_exceeded; - uma ocorrência não cria execução paralela. Se outra execução estiver ativa, o horário pode ser perdido e não há promessa de fila ou catch-up;
- não há retry automático. A falha atual encerra a execução e não cria outra tentativa.
Os status públicos são running, succeeded, failed, deadline_exceeded e cancelled. O número bruto do código de saída não faz parte do DTO público; use o status e os logs para diagnosticar.
Atualizar o horário
Altere somente o cron com:
PATCH /v1/project/:id/schedule{
"cron": "30 3 * * *"
}A resposta confirma o novo horário e informa timezone: UTC:
{
"status": "success",
"data": {
"cron": "30 3 * * *",
"timezone": "UTC"
}
}A permissão necessária é project.schedule.update. Uma expressão inválida retorna 400; uma tentativa de atualizar um projeto que não é um Job também é rejeitada.
Listar execuções
Consulte as execuções do ciclo de cobrança atual. A ordenação é da execução programada mais recente para a mais antiga:
GET /v1/project/:id/job-runs?page=1&limit=20&status=succeededpage começa em 1, limit aceita até 50 itens e status pode ser running, succeeded, failed, deadline_exceeded ou cancelled. A resposta contém runs e pagination:
{
"status": "success",
"data": {
"runs": [
{
"id": "507f1f77bcf86cd799439011",
"status": "succeeded",
"scheduled_at": "2026-09-01T03:00:00.000Z",
"started_at": "2026-09-01T03:00:02.000Z",
"finished_at": "2026-09-01T03:01:12.000Z",
"duration_seconds": 70,
"billed_minutes": 2,
"plan": "job-basic",
"amount": 4,
"currency": "brl"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 1,
"total_pages": 1
},
"cycle_started_at": "2026-09-01T00:00:00.000Z",
"next_reset_at": "2026-10-01T00:00:00.000Z"
}
}duration_seconds é o tempo de parede entre início e fim, apresentado em segundos inteiros. started_at e finished_at são o início e o fim reais do container: o tempo de agendamento e de download da imagem não é cobrado, e uma execução cujo container nunca iniciou não gera cobrança. billed_minutes é a unidade faturada, arredondada para cima com mínimo de 1 e máximo de 60; os dois campos não são equivalentes.
Na data indicada por next_reset_at, a listagem reinicia imediatamente, mesmo se o processamento financeiro estiver atrasado. Execuções antigas continuam armazenadas internamente para auditoria e cobrança, mas deixam de ser expostas na listagem, nos logs e nas métricas do ciclo atual.
Consultar custo do ciclo atual
Use a permissão project.billing.read para consultar o custo total das execuções que começaram no ciclo atual:
GET /v1/project/:id/job-runs/cost-summary{
"status": "success",
"data": {
"totals": [
{
"currency": "brl",
"total_amount": 120,
"executed_runs": 1,
"billed_minutes": 60,
"billed_runs": 1
}
],
"cycle_started_at": "2026-09-01T00:00:00.000Z",
"next_reset_at": "2026-10-01T00:00:00.000Z"
}
}total_amount está na unidade minoritária da moeda e soma os valores persistidos de cada execução terminal iniciada no ciclo. Cada valor é calculado a partir de billed_minutes e da tarifa precisa do produto capturada para aquela execução, sem arredondamento por execução; o amount público e a tarifa capturada não são recalculados quando o catálogo muda. Uma execução que atravessa a data de cobrança permanece no ciclo em que começou. O total aparece quando o uso terminal é materializado; ele não espera a liquidação financeira. Na próxima data de cobrança, totals, executed_runs, billed_minutes e o histórico público reiniciam. billed_runs é um alias depreciado de executed_runs, mantido temporariamente por compatibilidade. Registros financeiros antigos não são apagados.
Cancelar uma execução
Para encerrar uma execução ativa, use o ID do projeto e o ID público da execução:
POST /v1/project/:id/job-runs/:runId/cancelA operação exige project.job-run.cancel no projeto. Ela é idempotente: se a execução já estiver em succeeded, failed, deadline_exceeded ou cancelled, a API retorna o estado terminal atual sem iniciar outra cobrança; repetir uma solicitação depois de um cancelamento bem-sucedido retorna cancelled, e uma solicitação repetida enquanto o mesmo cancelamento ainda está em andamento aguarda a conclusão e retorna o mesmo estado final. O cancelamento afeta somente a execução ativa identificada por :runId; não pausa o cron nem impede horários futuros. Para impedir novas ocorrências, pause o projeto.
Para uma execução ativa compatível com cancelamento seguro, a API só responde com sucesso depois de confirmar que a execução parou. O encerramento primeiro permite até 30 segundos para uma parada graciosa; somente depois disso a limpeza forçada é solicitada. O estado final de uma operação bem-sucedida é cancelled, e a cobrança considera somente o intervalo entre started_at e finished_at, que é o momento da solicitação de cancelamento; o tempo de encerramento gracioso não é cobrado. Se a execução terminar enquanto a solicitação estiver em andamento, a API pode retornar o estado terminal observado em vez de cancelled. Se a execução já não existir mais no ambiente de execução quando for cancelada, ela é marcada como cancelled e cobrada somente até a última vez em que a Zenifra a viu em execução, nunca até o momento do cancelamento. Depois disso, o projeto pode ser excluído. Uma execução que ainda não iniciou não gera uso.
Execuções ativas mais antigas podem não ter suporte a cancelamento seguro. Nessa situação, a API retorna HTTP 409 com JOB_RUN_CANCELLATION_UNAVAILABLE e a mensagem This execution cannot be cancelled safely. Wait for it to finish or reach its time limit.. Aguarde a execução terminar ou atingir o limite de 60 minutos; as próximas execuções têm suporte ao cancelamento seguro. Essa resposta não é sucesso e não confirma que a execução parou. A cobrança já materializada permanece conforme as regras da execução e o tempo efetivamente consumido.
{
"status": "success",
"data": {
"run": {
"id": "507f1f77bcf86cd799439011",
"status": "cancelled",
"billed_minutes": 2,
"currency": "brl"
}
}
}O CLI oferece a mesma operação para uma execução específica, em uma destas formas:
zenifra project runs cancel --project <id> --run <id>
zenifra project runs cancel --project <id> --run <id> --jsonA operação não possui a opção --wait. Com --json, o CLI mantém os campos públicos atuais da execução, como ID, status, horários, duração, minutos faturados, plano, moeda e valor, e omite detalhes internos.
Consultar logs de uma execução
Use o ID do projeto e o ID público da execução:
GET /v1/project/:id/job-runs/:runId/logsA resposta vincula o texto à execução consultada:
{
"status": "success",
"data": {
"run": {
"id": "507f1f77bcf86cd799439011",
"status": "succeeded",
"billed_minutes": 2,
"currency": "brl"
},
"logs": "2026-09-01T03:00:02.000Z relatorio iniciado\\n2026-09-01T03:01:12.000Z relatorio concluido"
}
}Logs têm limite de 50 KiB por resposta. Para consultá-los, use project.logs.read no projeto. Um ID inexistente ou pertencente a um ciclo anterior retorna 404. Logs de build GitHub usam os endpoints da seção Fonte GitHub, não este endpoint.
Consultar métricas da execução
Use:
GET /v1/project/:id/job-runs/:runId/metricsA permissão necessária é project.metrics.read. O retorno usa o envelope { "status": "success", "data": ... }, e data tem exatamente estes campos públicos:
{
"run_id": "507f1f77bcf86cd799439011",
"status": "available",
"window": {
"started_at": "2026-09-01T03:00:02.000Z",
"finished_at": "2026-09-01T03:01:12.000Z"
},
"cpu": {
"average_cores": 0.25,
"peak_cores": 0.7
},
"memory": {
"average_bytes": 12000000,
"peak_bytes": 16777216
},
"samples": 7
}Para uma execução running, status é collecting e cpu/memory podem conter somente latest_cores/latest_bytes, além de sampled_at. Para uma execução terminal, status: available contém médias e picos. Quando não há amostra suficiente, a execução é curta ou a fonte está indisponível, status é unavailable, samples pode ser 0 e os campos desconhecidos ficam ausentes; nunca são convertidos em zero. O alvo de coleta ativa é aproximadamente 10 segundos, portanto uma execução menor que o primeiro intervalo pode permanecer unavailable. Métricas de um ciclo anterior retornam 404 pela API pública.
Cobrança e armazenamento
price_per_minute, amount e total_amount são números em centavos de BRL e podem ser fracionários. O JSON preserva esses valores numéricos; apresentações humanas podem usar até quatro casas decimais. currency é brl e payment_mode é per_minute. Cada valor terminal é armazenado exato, sem arredondamento para cima. Na cobrança do ciclo, o cartão recebe a parte inteira em centavos e a fração restante fica como saldo devedor para o próximo ciclo; nada é perdido nem arredondado para cima. A cobrança usa no mínimo 1 minuto inteiro e no máximo 60 minutos. A fórmula é:
amount = billed_minutes × price_per_minute
| Duração | duration_seconds | billed_minutes |
|---|---|---|
| 1 segundo | 1 | 1 |
| 59 segundos | 59 | 1 |
| 70 segundos | 70 | 2 |
| 60 minutos | 3.600 | 60 |
Falhas, cancelamentos depois do início e deadline_exceeded cobram o tempo consumido. Execução ativa não gera cobrança parcial e execução que não iniciou não gera uso. O armazenamento efêmero usa a capacidade configurada durante a execução e não adiciona cobrança de armazenamento. storage.persistent: true mantém os dados em dir_path_to_persist entre execuções e gera cobrança separada por GB-hora enquanto o projeto existir, inclusive pausado, até a exclusão.
Excluir um Job
Use o endpoint comum de exclusão de projetos:
DELETE /v1/project/:idA exclusão interrompe novos horários antes de verificar o histórico. Se existir uma execução ativa, uma execução ainda não observada ou uma execução concluída cuja cobrança ainda não foi registrada, a API não exclui o projeto e retorna 409 com code: JOB_RUNS_PENDING. Nesse caso:
- cancele uma execução ativa com
POST /v1/project/:id/job-runs/:runId/cancel, se necessário e autorizado; - aguarde o histórico refletir o estado terminal e os campos de cobrança;
- tente excluir o projeto novamente.
Não repita a exclusão em loop enquanto JOB_RUNS_PENDING continuar sendo retornado. Os registros internos de execução e métricas ficam retidos por até 90 dias; o uso materializado e os registros financeiros permanecem retidos para auditoria e não são apagados pelo reinício do ciclo. Os endpoints públicos expõem somente o ciclo de cobrança atual.
JOB_RUNS_PENDING pertence à exclusão do projeto; ele não é o erro de um cancelamento. Uma tentativa de cancelar sem suporte seguro retorna JOB_RUN_CANCELLATION_UNAVAILABLE, e a execução deve terminar ou atingir o limite de 60 minutos.
Status HTTP e indisponibilidade
| Status | Significado |
|---|---|
200 | Consulta ou atualização concluída |
201 | Job criado |
400 | Dados inválidos ou operação incompatível |
401 | Credencial ausente ou inválida |
403 | Organização, projeto ou permissão não autorizada |
404 | Projeto ou execução não encontrada |
409 | Operação não permitida no estado atual; o cancelamento seguro pode retornar JOB_RUN_CANCELLATION_UNAVAILABLE e a exclusão pode retornar JOB_RUNS_PENDING |
429 | Limite de requisições excedido |
500 | Falha ao processar a requisição |
503 | Jobs desabilitado; o catálogo retorna SCHEDULED_JOBS_UNAVAILABLE |
Troubleshooting
-
failed: leia os logs, confirme o executável e investigue o código de saída não zero. O código1é apenas um exemplo. -
deadline_exceeded: o processo não terminou em 60 minutos. Definacommand/argspara uma rotina finita ou use o endpoint de cancelamento. -
unavailableem métricas: não há amostra suficiente ou a fonte estava indisponível. Não trate esse estado como CPU ou memória zero. -
Nenhuma execução no horário esperado: confirme os cinco campos do cron e o UTC. Uma ocorrência perdida enquanto outra execução estava ativa não é enfileirada.
-
Falha no fluxo GitHub antes da execução: consulte
GET /v1/project/:id/github/buildseGET /v1/project/:id/github/builds/:buildId/logs; build e execução têm estados e históricos distintos. -
409 JOB_RUN_CANCELLATION_UNAVAILABLE: a execução não pode ser cancelada com segurança. Aguarde o estado terminal ou o limite de 60 minutos; não trate a resposta como sucesso ou confirmação de parada. -
409 JOB_RUNS_PENDINGao excluir: cancele quando autorizado, aguarde terminalidade e cobrança e repita uma vez.
Última atualização em