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

Os scopes mínimos são:

OperaçãoScopeRecurso
Consultar planosacesso à API de planos—
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-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 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": 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/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. 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çã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
403Organização, projeto ou permissão não autorizada
404Projeto ou execução não encontrada
409Operaçã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
429Limite de requisições excedido
500Falha ao processar a requisição
503Jobs 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ó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