> For the complete documentation index, see [llms.txt](https://docs.skail.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.skail.dev/construir/padroes-e-antipadroes/human-in-the-loop.md).

# Human-in-the-loop

Processos que precisam de uma pessoa no meio do caminho: o reembolso aprovado pelo gerente, o documento revisado pelo jurídico, a transação acima do limite liberada pelo compliance. O tempo de resposta de uma pessoa é o oposto do tempo de um sistema, e no skail isso é um `WaitForEvent` com prazo no meio da function.

## Quando usar

Toda vez que o fluxo depende de uma decisão humana. Sem o skail isso vira tabela de pendências, job para achar quem demorou, e-mail de lembrete, endpoint para receber a decisão e lógica para destravar o fluxo original. Aqui, a function espera, a tela dispara o evento, e o prazo é uma linha.

## Aprovação com prazo

Uma solicitação precisa ser decidida em 48 horas; se não for, é cancelada e quem solicitou é avisado.

```csharp
public static class Eventos
{
    public const string ReembolsoDecidido = "REEMBOLSO_DECIDIDO";
}

public record DecisaoAprovacao(bool Aprovado, string DecididoPor, string? Justificativa);

[SkailFunction]
public async SkailTask SolicitarReembolso(Guid solicitacaoId, Guid aprovadorId)
{
    await RegistrarSolicitacao(solicitacaoId);                              // commands
    await NotificarAprovador(aprovadorId, solicitacaoId);

    var decisao = SkailTask.WaitForEvent<DecisaoAprovacao>(Eventos.ReembolsoDecidido, solicitacaoId.ToString());
    var prazo   = SkailTask.Delay(TimeSpan.FromHours(48));

    if (await SkailTask.WhenAny(decisao, prazo) == prazo)
    {
        await CancelarPorExpiracao(solicitacaoId);
        await NotificarSolicitante(solicitacaoId, "expirou");
        return;
    }

    var resposta = await decisao;
    if (resposta.Aprovado) await EfetuarReembolso(solicitacaoId, resposta.DecididoPor);
    else                   await RegistrarRecusa(solicitacaoId, resposta.DecididoPor, resposta.Justificativa);
}
```

A tela do aprovador, no clique em aprovar ou recusar, chama um endpoint do seu backend. O endpoint não grava nada: só repassa a decisão ao skail, disparando o evento pela API HTTP. Quem grava é a function, em `EfetuarReembolso` ou `RegistrarRecusa`, com o payload que veio no evento:

```csharp
// Endpoint chamado pela interface do aprovador (ASP.NET comum, fora do skail)
// _skail: HttpClient com BaseAddress, skail-key e Skail-namespace configurados
await _skail.PostAsJsonAsync($"/api/v1/fire/{Eventos.ReembolsoDecidido}/{solicitacaoId}", decisao);
```

Gravar a decisão no endpoint e só depois disparar o evento parece inofensivo, mas deixa dois registros que podem divergir: se o disparo falhar, a decisão está no banco e a function continua esperando, até cancelar por prazo um reembolso que foi aprovado. Dentro da function, a gravação é um command: se o banco falhar, o skail retenta; se a aplicação cair, a execução retoma com o evento já recebido. O endpoint fica com uma única responsabilidade, entregar a decisão; se o POST falhar, ele devolve erro e a tela pede para tentar de novo.

Ver [Como disparar um evento pela API HTTP](/construir/escrever-fluxos/como-disparar-um-evento-pela-api-http.md).

## Aprovação em cadeia

Vários níveis conforme o valor: gerente, diretor, VP. Uma sequência de esperas, cada uma liberando a próxima.

```csharp
[SkailFunction]
public async SkailTask SolicitarGastoExcepcional(Guid solicitacaoId, decimal valor)
{
    await RegistrarSolicitacao(solicitacaoId, valor);

    if (!await AguardarAprovacao(solicitacaoId, "GERENTE", TimeSpan.FromHours(24))) return;
    if (valor > 10_000  && !await AguardarAprovacao(solicitacaoId, "DIRETOR", TimeSpan.FromHours(48))) return;
    if (valor > 100_000 && !await AguardarAprovacao(solicitacaoId, "VP", TimeSpan.FromDays(5))) return;

    await LiberarGasto(solicitacaoId);
}

[SkailFunction]
public async SkailTask<bool> AguardarAprovacao(Guid solicitacaoId, string nivel, TimeSpan prazo)
{
    await NotificarAprovadoresDoNivel(solicitacaoId, nivel);

    var decisao   = SkailTask.WaitForEvent<DecisaoAprovacao>($"APROVACAO_{nivel}", solicitacaoId.ToString());
    var expiracao = SkailTask.Delay(prazo);

    if (await SkailTask.WhenAny(decisao, expiracao) == expiracao)
    {
        await CancelarPorExpiracao(solicitacaoId, nivel);
        return false;
    }

    var resposta = await decisao;
    if (!resposta.Aprovado)
    {
        await RegistrarRecusa(solicitacaoId, nivel, resposta.Justificativa);
        return false;
    }
    return true;
}
```

Cada nível tem o próprio nome de evento (`APROVACAO_GERENTE`, `APROVACAO_DIRETOR`), então a tela de cada aprovador dispara o evento certo. A hierarquia fica explícita no método, não em tabelas de configuração; lendo o código, qualquer pessoa entende em que condições cada nível é consultado. Os `if` decidem sobre `valor`, um argumento, e sobre o retorno de `AguardarAprovacao`, que está no histórico: determinístico.

## Escalonamento automático

Notificar o primeiro aprovador e, se ele não responder no prazo, passar ao próximo da cadeia, sem que ninguém acompanhe manualmente.

```csharp
[SkailFunction]
public async SkailTask SolicitarAprovacaoComEscalonamento(Guid solicitacaoId, Guid[] cadeia)
{
    // uma única espera, criada antes do laço: qualquer aprovador da cadeia dispara o mesmo evento
    var decisao = SkailTask.WaitForEvent<DecisaoAprovacao>(Eventos.AprovacaoCadeia, solicitacaoId.ToString());

    foreach (var aprovador in cadeia)
    {
        await NotificarAprovador(aprovador, solicitacaoId);

        var prazoIndividual = SkailTask.Delay(TimeSpan.FromHours(24));
        if (await SkailTask.WhenAny(decisao, prazoIndividual) == decisao)
        {
            var resposta = await decisao;
            if (resposta.Aprovado) await ExecutarSolicitacao(solicitacaoId);
            else                   await RegistrarRecusa(solicitacaoId, resposta);
            return;
        }

        await RegistrarNaoResposta(solicitacaoId, aprovador);
    }

    await CancelarSemAprovacao(solicitacaoId);
}
```

Um detalhe que importa: a espera é criada uma vez, antes do laço, e reutilizada em cada `WhenAny`. Se você criasse um `WaitForEvent` novo a cada iteração com o mesmo nome e id, o fire do segundo aprovador atenderia a primeira espera (a que perdeu a corrida e ficou pendente), e a iteração corrente não veria a decisão. Uma espera, vários prazos.

## O que a pessoa vê

O aprovador não sabe que existe o skail: ele recebe a notificação, abre a tela, clica. A tela chama o seu backend, o backend faz o fire. Se ele clicar depois do prazo, o fire encontra a espera já atendida pelo timeout (ou nenhuma espera pendente) e o seu backend decide o que dizer a ele; a function já seguiu pelo caminho da expiração.

## Próximos passos

[Como esperar com timeout](/construir/escrever-fluxos/como-esperar-com-timeout.md) detalha o prazo e o lembrete antes de vencer. [Fluxo de aprovação com timeout](/aprender/exemplos-completos/fluxo-de-aprovacao-com-timeout.md) é o exemplo completo. [Eventos externos e correlação](/aprender/fundamentos/eventos-externos-e-correlacao.md) para o mecanismo.
