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 (x-api-key ou Authorization: Bearer znf_...) ou um token de usuário. Com token de usuário, informe a organização em x-organization-id; com API Key da organização esse header é dispensável. Envie Content-Type: application/json nas requisições com corpo.

x-api-key: sua-api-key
Content-Type: application/json

Os scopes mínimos são:

OperaçãoScopeRecurso
Consultar planosNenhum (rota pública)—
Criar um Job Docker/OCI ou GitHubproject.createorganization:*
Atualizar o cronproject.schedule.updateproject:<project-id>
Listar execuçõesproject.readproject:<project-id>
Consultar o custo do cicloproject.billing.readproject:<project-id>
Cancelar uma execução ativaproject.job-run.cancelproject:<project-id>
Consultar logs da execução ou builds GitHubproject.logs.readproject:<project-id>
Consultar métricas da execuçãoproject.metrics.readproject:<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/plans

A resposta usa o catálogo de produto dedicado a Jobs. Os IDs têm o prefixo job-, como job-starter, job-basic e job-premium. 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": 0.08333333,
      "currency": "brl",
      "payment_mode": "per_minute",
      "features": ["Até 60 minutos por execução"],
      "permissions": {
        "free_storage_size": "5"
      }
    }
  ]
}

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

O Idempotency-Key é opcional para Jobs, aceita de 16 a 200 caracteres (A-Z, a-z, 0-9, ., _ e -).

{
  "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. A CLI 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 0 resulta em succeeded;
  • qualquer código não zero resulta em failed; 1 é somente um exemplo;
  • sem command e args, o entrypoint e o CMD da imagem são usados;
  • um processo que não termina permanece running até cancelamento ou até o deadline de 60 minutos, quando vira deadline_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=succeeded

page 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": 0.16666666,
        "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 da execução: o tempo de agendamento e de download da imagem não é cobrado, e uma execução que 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": 0.16666666,
        "executed_runs": 1,
        "billed_minutes": 2,
        "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/cancel

A 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> --json

A 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/logs

A 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/metrics

A permissão necessária é project.metrics.read, e o plano do Job precisa incluir métricas por execução (hoje, o job-premium). Em um plano sem métricas, a API responde 402 com JOB_PLAN_METRICS_UNAVAILABLE. 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é seis casas decimais para o preço por minuto e até quatro para valores e totais. currency é brl e payment_mode é per_minute. Cada valor terminal é armazenado exato, sem arredondamento para cima. Na cobrança do ciclo, é cobrada 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çãoduration_secondsbilled_minutes
1 segundo11
59 segundos591
70 segundos702
60 minutos3.60060

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/:id

A 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:

  1. cancele uma execução ativa com POST /v1/project/:id/job-runs/:runId/cancel, se necessário e autorizado;
  2. aguarde o histórico refletir o estado terminal e os campos de cobrança;
  3. 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

StatusSignificado
200Consulta ou atualização concluída
201Job criado
400Dados inválidos ou operação incompatível
401Credencial ausente ou inválida
402O plano do Job não inclui métricas por execução (JOB_PLAN_METRICS_UNAVAILABLE)
403Organização, projeto ou permissão não autorizada
404Projeto ou execução não encontrada
409Operação não permitida no estado atual; a configuração do Job está indisponível; os logs da execução ainda não estão disponíveis (run logs are not available); o cancelamento seguro pode retornar JOB_RUN_CANCELLATION_UNAVAILABLE e a exclusão pode retornar JOB_RUNS_PENDING
429Limite de requisições excedido
500Falha ao processar a requisição
503Jobs temporariamente indisponível; todas as rotas de Jobs retornam SCHEDULED_JOBS_UNAVAILABLE

Limites de requisições

Contados por IP de origem.

OperaçãoLimite
GET /v1/project/job/plans60 por minuto
POST /v1/project10 por minuto
PATCH /v1/project/:id/schedule10 a cada 5 minutos
GET /v1/project/:id/job-runs e /job-runs/cost-summary100 por minuto
POST /v1/project/:id/job-runs/:runId/cancel20 por minuto
GET /v1/project/:id/job-runs/:runId/logs e /metrics200 por minuto

Troubleshooting

  • failed: leia os logs, confirme o executável e investigue o código de saída não zero. O código 1 é apenas um exemplo.

  • deadline_exceeded: o processo não terminou em 60 minutos. Defina command/args para uma rotina finita ou use o endpoint de cancelamento.

  • unavailable em 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/builds e GET /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_PENDING ao excluir: cancele quando autorizado, aguarde terminalidade e cobrança e repita uma vez.

  • Guia de Jobs agendados

  • API de métricas e logs

  • Builds GitHub

Última atualização em

Nessa página