> ## Documentation Index
> Fetch the complete documentation index at: https://docs.jelou.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Execuções diferidas

> Agende uma execução futura da sua função: lembretes, follow-ups e carrinhos abandonados com ctx.jelou.schedule, cancelamento e guards de disparo.

Uma **execução diferida** é uma invocação que você agenda para acontecer uma única vez no futuro: um lembrete em 24 horas, um follow-up de carrinho abandonado, uma pesquisa 2 dias após a compra.

<Info>
  **Cron ou diferida?**

  * **[Cron](/pt/guias/funcoes/cron)** — repete em um horário fixo (todos os dias às 9:00). Definido no código.
  * **Diferida** — acontece uma vez, em um momento calculado em tempo de execução (24 horas depois *deste* pedido). Agendada pelo handler ou pela requisição HTTP.
</Info>

## Agendar pelo handler

`ctx.jelou.schedule()` agenda uma execução relativa a "agora":

```typescript index.ts theme={null}
import { define, z } from "@jelou/functions";

export default define({
  name: "carrinho-abandonado",
  description: "Recebe o evento de carrinho abandonado e agenda o follow-up",
  input: z.object({
    userId: z.string(),
    carrinhoId: z.string(),
  }),
  handler: async (input, ctx) => {
    const booking = await ctx.jelou.schedule({
      in: "24h",
      path: "/enviar-followup",
      // Inclua o botId: no disparo não há canal resolvido
      payload: { userId: input.userId, carrinhoId: input.carrinhoId, botId: ctx.bot.id },
      key: `carrinho:${input.userId}:${input.carrinhoId}`,
      subject: `user_${input.userId}`,
    });

    ctx.log("Follow-up agendado", { id: booking.id, quando: booking.scheduledAt });
    return { agendado: true, id: booking.id };
  },
});
```

### Parâmetros

| Campo            | Tipo               | Obrigatório | Descrição                                                |
| ---------------- | ------------------ | ----------- | -------------------------------------------------------- |
| `in`             | `string \| number` | Sim         | Duração: `"30m"`, `"24h"`, `"1h30m"`, `"3d"` ou segundos |
| `path`           | `string`           | Sim         | Rota da sua função que será executada. Começa com `/`    |
| `payload`        | `object \| array`  | Não         | Dados que o handler recebe no disparo                    |
| `key`            | `string`           | Não         | Etiqueta para buscar ou cancelar depois                  |
| `subject`        | `string`           | Não         | Audiência, normalmente o ID do usuário                   |
| `idempotencyKey` | `string`           | Não         | Evita agendar duas vezes em retentativas                 |
| `pinDeployment`  | `boolean`          | Não         | Fixa a execução na implantação atual                     |

A duração aceita unidades compostas da maior para a menor: `"1h30m"` é válido, `"30m1h"` não.

<Warning>
  Para follow-ups muito curtos use **5 segundos ou mais**. Abaixo disso, a latência de rede pode deixar o momento agendado no passado e a plataforma rejeita.
</Warning>

### Momento absoluto

`ctx.jelou.scheduleAt()` recebe uma data em vez de uma duração:

```typescript theme={null}
await ctx.jelou.scheduleAt({
  at: new Date("2026-12-24T18:00:00Z"), // ou a string ISO 8601
  path: "/felicitacao-natal",
  payload: { userId: input.userId },
});
```

Lança erro se a data já passou ou não pode ser interpretada. Os outros campos funcionam como em `schedule()`.

### Evitar duplicados

Se o serviço que chama sua função fizer retentativa, `idempotencyKey` garante um único agendamento:

```typescript theme={null}
const booking = await ctx.jelou.schedule({
  in: "24h",
  path: "/enviar-followup",
  payload: { userId: input.userId, carrinhoId: input.carrinhoId },
  idempotencyKey: `carrinho-abandonado:${input.eventoId}`,
});

if (booking.idempotentReplay) {
  ctx.log("Já estava agendado, não duplicou", { id: booking.id });
}
```

Reutilizar a mesma `idempotencyKey` com um `payload` diferente retorna erro `409`.

## Agendar por requisição HTTP

Qualquer cliente pode diferir uma chamada adicionando um header ao POST normal. Sem headers de agenda, a função executa imediatamente como sempre.

```bash theme={null}
curl -X POST https://lembretes.fn.jelou.ai/enviar-lembrete \
  -H "Authorization: Bearer <sua-api-key>" \
  -H "X-Jelou-Schedule-In: 5m" \
  -H "X-Jelou-Key: lembrete:user-42" \
  -H "Content-Type: application/json" \
  -d '{"userId":"42","mensagem":"Sua consulta é amanhã"}'
```

| Header                   | Efeito                                           |
| ------------------------ | ------------------------------------------------ |
| `X-Jelou-Schedule-In`    | Duração (`"5m"`, `"1h30m"`, `"24h"` ou segundos) |
| `X-Jelou-Schedule-At`    | Momento absoluto em ISO 8601                     |
| `X-Jelou-Key`            | Etiqueta para buscar ou cancelar depois          |
| `X-Jelou-Subject`        | Audiência, normalmente o ID do usuário           |
| `X-Jelou-Pin-Deployment` | `"true"` para fixar a implantação atual          |
| `Idempotency-Key`        | Evita agendar duas vezes em retentativas         |

A resposta é `202 Accepted` quando o agendamento é criado e `200 OK` quando uma retentativa reutiliza um agendamento existente. O corpo não pode passar de **64 KB**.

## Receber o disparo

Na hora marcada, sua função recebe o `payload` original na rota indicada. Use `ctx.isScheduledFire` para distinguir o disparo de uma requisição normal:

```typescript theme={null}
handler: async (input, ctx) => {
  if (ctx.isScheduledFire) {
    ctx.log("Execução diferida disparada");
  }
  // ...
}
```

### Verificar antes de agir

Entre o agendamento e o disparo, a realidade pode mudar: o cliente já pagou, o template foi pausado, o usuário pediu descadastro. `ctx.guard` encadeia essas verificações e evita o envio se alguma falhar:

```typescript theme={null}
export default define({
  name: "enviar-followup",
  description: "Envia o follow-up de carrinho abandonado se ainda se aplica",
  input: z.object({
    userId: z.string(),
    carrinhoId: z.string(),
    botId: z.string(),
  }),
  handler: async (input, ctx) => {
    // O botId viajou no payload que você agendou antes
    const registro = ctx.templateRegistry.for(input.botId);

    return ctx.guard
      .when("semPagar", async () => !(await consultarCarrinho(input.carrinhoId)).pago)
      .when("templateAprovado", () => registro.has("carrinho_abandonado_v3"))
      .run(async () => {
        await ctx.jelou.sendTemplate({
          template: "carrinho_abandonado_v3",
          to: input.userId,
          params: ["María", `https://loja.com/recuperar?c=${input.carrinhoId}`],
        });
        return { enviado: true };
      });
  },
});
```

Se todas as verificações passam, `.run()` executa e o resultado é a resposta. Se alguma falha, a cadeia é interrompida e retorna `{ skipped: "<nome>" }` sem executar o envio.

<Note>
  A cadeia termina com `.run(handler)`, não com `.then()`. `ctx.guard` é uma cadeia nova em cada requisição.
</Note>

<Warning>
  Se o bot não viaja no `payload`, em um disparo diferido `ctx.bot` não está resolvido e `ctx.templateRegistry` fica sem canal associado. Inclua o `botId` no `payload` ao agendar e vincule com `ctx.templateRegistry.for(botId)`. Veja [templates do WhatsApp](/pt/guias/funcoes/mensajeria#validar-templates-antes-de-enviar).
</Warning>

## Consultar o que está agendado

`ctx.jelou.findDeferred()` lista as execuções pendentes da sua função:

```typescript theme={null}
const { data, total } = await ctx.jelou.findDeferred({
  subject: `user_${input.userId}`,
  status: "scheduled",
});

for (const linha of data) {
  ctx.log("pendente", linha.id, linha.scheduledAt, linha.key);
}
```

| Campo              | Descrição                                                                             |
| ------------------ | ------------------------------------------------------------------------------------- |
| `key`              | Filtra por etiqueta exata                                                             |
| `subject`          | Filtra por audiência                                                                  |
| `status`           | `"active"` (default), `"scheduled"`, `"firing"`, `"fired"`, `"cancelled"`, `"failed"` |
| `page` / `perPage` | Paginação. Default `20`, máximo `100`                                                 |

## Cancelar

`ctx.jelou.cancelDefer()` cancela em lote por audiência ou etiqueta. Exige exatamente um de `subject`, `key` ou `keyPrefix`:

```typescript theme={null}
// Descadastro do usuário: cancela todos os lembretes pendentes
const { cancelled, raced } = await ctx.jelou.cancelDefer({
  subject: `user_${input.userId}`,
});

ctx.log("descadastro processado", { cancelados: cancelled.length, em_curso: raced.length });
```

`raced` contém as execuções que já estavam disparando quando o cancelamento chegou — para essas, a verificação com `ctx.guard` no handler é a última defesa. Escreva seus handlers de forma idempotente.

Antes de um cancelamento amplo, previsualize o alcance com `dryRun`:

```typescript theme={null}
const previa = await ctx.jelou.cancelDefer({
  keyPrefix: "promo-verao:",
  dryRun: true,
});

if (previa.wouldCancelCount < 5000) {
  await ctx.jelou.cancelDefer({ keyPrefix: "promo-verao:" });
}
```

<Note>
  Não existe "reagendar": um agendamento é imutável. Para mudar a hora, cancele e agende de novo.
</Note>

## Teste local

`schedule`, `scheduleAt`, `findDeferred` e `cancelDefer` funcionam apenas em uma função **implantada**. Localmente lançam erro porque não há credenciais de plataforma.

Para exercitar o fluxo com `jelou functions dev`, ative a simulação:

```bash theme={null}
JELOU_FN_DEFER_DEV=1 jelou functions dev
```

Nesse modo os argumentos são validados, o agendamento é registrado nos logs e um resultado sintético é retornado — seu handler continua executando, mas nada real é agendado.

Para testes unitários use [`createMockContext`](/pt/guias/funcoes/testing), cujo `ctx.jelou` registra as chamadas sem rede nem configuração.

## Inspecionar pelo CLI

```bash theme={null}
# Agendamentos ativos da função do projeto atual
jelou functions defer list

# Os que falharam
jelou functions defer list minha-fn --status failed

# Os de um usuário
jelou functions defer list minha-fn --subject user_42

# Detalhe de um agendamento (inclui o último erro)
jelou functions defer get dinv_01ksqbvyqpe0prt91exc97mh4n
```

Veja a [referência do CLI](/pt/guias/funcoes/cli#execuções-diferidas).

## Limites

| Limite                           | Valor                   |
| -------------------------------- | ----------------------- |
| Agendamentos ativos por empresa  | 1.000                   |
| Antecedência máxima              | 30 dias                 |
| Tamanho do `payload`             | 64 KB                   |
| Cancelamento em lote por chamada | 10.000 correspondências |

Agendamentos já disparados, cancelados ou falhos não contam no limite de ativos.

## Problemas comuns

<Tabs>
  <Tab title="Não disparou">
    Consulte o status e o último erro do agendamento:

    ```bash theme={null}
    jelou functions defer get dinv_01ksqbvy... --payload
    ```

    Se o status é `failed`, o campo de erro indica por que a entrega falhou. Se é `cancelled`, algo cancelou antes — revise suas chamadas a `cancelDefer`.
  </Tab>

  <Tab title="Enviou mesmo tendo cancelado">
    Um cancelamento que chega quando a execução já começou não a interrompe. Aparece em `raced` e seu handler executa.

    Por isso a verificação vai no handler, não só no cancelamento:

    ```typescript theme={null}
    return ctx.guard
      .when("segueAtivo", async () => await usuarioAtivo(input.userId))
      .run(async () => { /* envio */ });
    ```
  </Tab>

  <Tab title="Erro em local">
    ```
    scheduleAt unavailable: platform credentials missing
    ```

    Você está chamando a família `schedule` em local. Suba o servidor com `JELOU_FN_DEFER_DEV=1 jelou functions dev` para simular os agendamentos.
  </Tab>

  <Tab title="Agendou duas vezes">
    O serviço que chama sua função fez retentativa. Adicione `idempotencyKey` (ou o header `Idempotency-Key`) com um valor derivado do evento:

    ```typescript theme={null}
    idempotencyKey: `carrinho-abandonado:${input.eventoId}`
    ```
  </Tab>
</Tabs>

<CardGroup cols={2}>
  <Card title="Cron" icon="clock" href="/pt/guias/funcoes/cron">
    Tarefas recorrentes em horário fixo.
  </Card>

  <Card title="Mensageria" icon="message" href="/pt/guias/funcoes/mensajeria">
    Enviar WhatsApp e validar templates.
  </Card>

  <Card title="Webhooks" icon="shield-check" href="/pt/guias/funcoes/webhooks">
    Verificar assinaturas de serviços externos.
  </Card>

  <Card title="CLI" icon="terminal" href="/pt/guias/funcoes/cli">
    Comandos `defer list` e `defer get`.
  </Card>
</CardGroup>
