> Índice da documentação para agentes: https://gaio.social/developers/llms.txt

# Montar automações pela API

Como escrever o corpo de uma automação (o canal, o gatilho e o grafo de blocos), com o catálogo dos blocos, as variáveis, exemplos prontos e os erros da validação.

## A definição de uma automação

Uma automação é um JSON com seis campos:

| Campo | Obrigatório | O que é |
| --- | --- | --- |
| `title` | sim | O nome da automação, de 1 a 128 caracteres. |
| `description` | não | Uma descrição, de até 512 caracteres. |
| `channel` | sim | O canal em que a automação roda (veja [Canal](https://gaio.social/developers/automacoes.md#canal)). |
| `trigger` | sim | O que inicia a automação (veja [Gatilho](https://gaio.social/developers/automacoes.md#gatilho)). |
| `graph` | não | Os blocos e as ligações entre eles (veja [O grafo](https://gaio.social/developers/automacoes.md#o-grafo)). Sem ele, a automação nasce só com o bloco inicial. |
| `folder_id` | não | A pasta em que a automação fica (veja [Pastas](https://gaio.social/developers/automacoes.md#pastas)). Sem ela, a automação fica fora de pastas. |

A automação mais simples responde uma mensagem direta do Instagram que contém "oi" ou "olá":

```json
{
  "title": "Boas-vindas",
  "channel": { "type": "instagram" },
  "trigger": { "events": ["message"], "match": "contains", "keywords": ["oi", "olá"] },
  "graph": {
    "nodes": {
      "root": { "type": "root", "data": { "next": ["saudacao"] } },
      "saudacao": {
        "type": "message",
        "data": {
          "text": "Oi, {contact.first_name}! Que bom ter você por aqui. Em que podemos ajudar?",
          "next": [null]
        }
      }
    }
  }
}
```

O `root` é o início, e o `next` dele aponta para o bloco `saudacao`, que envia o texto. O `null` no `next` da `saudacao` encerra a execução. O `{contact.first_name}` é uma [variável](https://gaio.social/developers/automacoes.md#variáveis), trocada pelo primeiro nome do contato no envio.

Este e os outros [exemplos](https://gaio.social/developers/automacoes.md#exemplos) desta página passam na validação, são criados e são publicados pelos testes da API, então você pode partir deles.

### Canal

`channel.type` é `instagram` ou `whatsapp`, e `channel.connection_id` é o id de uma conexão da conta, de `GET /v1/connections`.

- **WhatsApp**: `connection_id` é obrigatório e precisa ser um número de WhatsApp da conta. A API aceita e publica um número desconectado, mas a automação não responde ninguém enquanto ele estiver assim: confira o `status` em `GET /v1/connections`.
- **Instagram**: `connection_id` é opcional. Sem ele, a automação roda na conta do Instagram conectada.

Uma automação publicada não muda de canal: a resposta é `409`, com `workflow_published`. Despublique antes.

### Gatilho

O `trigger` diz que eventos iniciam a automação e como o texto deles é comparado com as palavras-chave.

| Campo | O que é |
| --- | --- |
| `events` | Os eventos que iniciam a automação (tabela abaixo). Com `[]`, nenhum evento inicia a automação: ela começa pela API (`POST /v1/workflows/{id}/runs`), pelo console (num envio para um contato, num envio em massa ou num iniciador de conversa do Instagram) ou por um bloco [`start`](https://gaio.social/developers/automacoes.md#start) de outra automação. |
| `match` | `any` (qualquer texto; as palavras-chave são ignoradas), `contains`, `equals` ou `starts_with`. |
| `keywords` | Opcional, padrão `[]`. Até 200 palavras-chave, de 1 a 200 caracteres. A mesma lista vale para todos os eventos, e basta uma palavra-chave bater. |
| `posts` | Opcional. `null` ou omitido (qualquer post), ou de 1 a 200 itens `{ "type": "feed", "id": "…" }` ou `{ "type": "story", "id": "…" }`, com o id da mídia no Instagram. Os posts do feed limitam o `comment`, e os stories limitam o `story_message`. |
| `next_post` | Opcional, padrão `false`. `true` limita o `comment` ao próximo post publicado depois de criar a automação. Não combina com posts do feed em `posts`. |
| `replies_list_id` | Opcional. A lista de respostas públicas aos comentários que iniciam a automação (veja [Listas de respostas](https://gaio.social/developers/automacoes.md#listas-de-respostas)). Só no Instagram e só com o evento `comment`: com outros eventos, ela é aceita, mas nunca é usada. |

A comparação com as palavras-chave não diferencia maiúsculas e minúsculas, mas diferencia acentos: `horario` não bate com "horário", então liste as duas grafias. Em `contains`, a palavra-chave vale em qualquer parte do texto, inclusive dentro de outra palavra: `oi` bate com "boa noite". `equals` e `starts_with` comparam o texto como chegou, sem tirar espaços nem pontuação: "oi!" não é igual a `oi`.

| Evento | Quando acontece | Canal |
| --- | --- | --- |
| `comment` | Um comentário num post ou reel. | Instagram |
| `live_comment` | Um comentário numa live. | Instagram |
| `story_message` | Uma resposta a um story. | Instagram |
| `story_mention` | Uma menção à conta no story de alguém. Não usa palavras-chave. | Instagram |
| `message` | Uma mensagem direta do contato. | Instagram e WhatsApp |
| `message_sent` | Uma mensagem direta que a própria conta enviou. | Instagram |

No WhatsApp, só `message` inicia uma automação. Os outros eventos são aceitos, mas nunca disparam, e a validação avisa com `trigger_event_not_supported`.

Numa automação iniciada por `comment` ou `live_comment`, a primeira mensagem sai como resposta privada ao comentário, que não depende da janela de 24 horas. O Instagram aceita uma só resposta privada por comentário: uma segunda mensagem antes de o contato responder é recusada, e a execução termina ali. Por isso, depois da primeira mensagem, use botões ou uma pergunta, que esperam o contato responder. Daí em diante, as mensagens saem como mensagens diretas.

Um `match` diferente de `any` sem palavras-chave também é aceito, mas os eventos que comparam texto nunca disparam (aviso `trigger_has_no_keywords`). O `story_mention` não usa palavras-chave e dispara mesmo assim.

## O grafo

`graph.nodes` é um mapa de blocos por id. Cada bloco tem `type`, o tipo do bloco, `data`, os campos daquele tipo, e `position`, opcional:

```json
"pedir_email": {
  "type": "question",
  "data": {
    "text": "Qual é o seu e-mail?",
    "answer_type": "any",
    "answer_field": "email",
    "next": ["confirmar"]
  },
  "position": { "x": 510, "y": 0 }
}
```

As regras:

- **O início**: o grafo precisa de um bloco com o id `root` e o tipo `root`. A execução começa nele.
- **Os ids**: letras minúsculas e números, com `_` entre as partes, até 64 caracteres. `pedir_email` e `passo2` valem; `pedirEmail`, `pedir-email`, `_passo` e `pedir__email` não.
- **As saídas**: `data.next` tem exatamente uma entrada por saída do bloco, na ordem das saídas, com o id do próximo bloco. `null` numa saída encerra a execução por ali. Um bloco final tem `"next": [null]`, nunca `[]`. A quantidade de saídas de cada tipo está na tabela abaixo.
- **Os caminhos**: um `next` pode voltar a um bloco que já rodou, mas cada bloco roda no máximo 6 vezes na mesma execução. Na 7ª vez, a execução para ali, com o status `stopped`. Um bloco que não se alcança a partir do `root` é conferido, mas nunca roda.
- **As posições**: `position` (`{ "x": …, "y": … }`) só serve para o editor do console desenhar o bloco. Sem ela, a API calcula uma. Ao reenviar um grafo que você leu, mantenha as posições, para não desarrumar o editor. Os campos `group_id` e `media_kind`, que aparecem em alguns blocos, também só servem para o editor.
- **O tamanho**: até 500 blocos e 256 KiB, contando as posições que a API preenche.
- **Os recursos**: as etiquetas, os campos personalizados e os uploads citados nos blocos precisam existir na conta. O grafo não cria nenhum deles. Crie as etiquetas com `POST /v1/tags` antes de salvar; os campos personalizados e os uploads são criados no console.
- **Os ids internos**: ao reescrever um bloco, mantenha o `id` dos botões de `buttons` e de `cards`, porque as métricas de clique são contadas por ele. O `id` das opções de um `randomizer` e dos canais de um `multichannel` só serve para o editor, mas o `randomizer` conta as escolhas pela posição de cada opção: não reordene as opções de um `randomizer` que já rodou.
- **O que você não conhece**: um tipo de bloco ou um campo que o seu código não conhece deve voltar como veio.

| Bloco | Saídas em `next` |
| --- | --- |
| [`buttons`](https://gaio.social/developers/automacoes.md#buttons) | 4: uma por botão, na ordem dos botões, e a 4ª, que segue logo depois do envio. |
| [`cards`](https://gaio.social/developers/automacoes.md#cards) | 31: três por card, uma por botão, e a 31ª, que segue logo depois do envio. |
| [`conditional`](https://gaio.social/developers/automacoes.md#conditional) | 2: condição verdadeira e condição falsa. |
| [`invoke`](https://gaio.social/developers/automacoes.md#invoke) | 2: sucesso e falha. |
| [`randomizer`](https://gaio.social/developers/automacoes.md#randomizer) | Uma por opção. |
| [`multichannel`](https://gaio.social/developers/automacoes.md#multichannel) | Uma por canal, e mais uma para quando nenhum canal pode receber. |
| [`blank`](https://gaio.social/developers/automacoes.md#blank) | 1, ou nenhuma, sem o campo `next`. |
| Os outros | 1. |

## Catálogo de blocos

O catálogo completo, com todos os campos, os valores aceitos e os tamanhos máximos, está em JSON Schema na [cópia do catálogo](https://gaio.social/developers/workflow-graph-schema.json) e em `GET /v1/workflow-graph-schema` (veja [O catálogo em JSON Schema](https://gaio.social/developers/automacoes.md#o-catálogo-em-json-schema)). As regras que olham mais de um campo não estão no schema e são conferidas pela validação (veja [Erros de validação](https://gaio.social/developers/automacoes.md#erros-de-validação)). Abaixo, o que cada bloco faz. Os campos marcados como opcionais podem ficar de fora; os outros são obrigatórios.

Fora dos `headers` e dos `fields` do `invoke`, os mínimos de 1 caractere desta seção não estão no catálogo e não são conferidos pela validação: um `text`, um `subtitle` ou um `title` de botão vazio é aceito, e o envio falha quando a automação roda. Se o plano da conta inclui a assinatura da Gaio, um `text` ou um `subtitle` vazio sai só com a assinatura. No Instagram, o `title` de cada card também precisa de pelo menos 1 caractere.

### `root`

O início da automação. Não faz nada: segue para a sua única saída.

- `next`: 1 saída.

### `message`

Envia um texto.

- `text`: de 1 a 4096 caracteres, com [variáveis](https://gaio.social/developers/automacoes.md#variáveis). No Instagram, só os primeiros 1000 caracteres são entregues. Quando o plano da conta inclui a assinatura da Gaio, o texto é cortado para caber com ela, nos dois canais.
- `next`: 1 saída, depois do envio.

### `question`

Envia uma pergunta e espera a próxima mensagem do contato, por até 90 dias.

- `text`: a pergunta, de 1 a 4096 caracteres, com variáveis. A validação não confere esse máximo: um texto mais longo é aceito, e o envio falha quando a automação roda. Quando o plano da conta inclui a assinatura da Gaio, o texto é cortado em vez de falhar. Como no `message`, o Instagram entrega só os primeiros 1000 caracteres, e menos com a assinatura.
- `answer_type`: sempre `any`: a pergunta aceita qualquer resposta. `number` está obsoleto: é aceito e roda como `any`, sem conferir se a resposta é um número, e a validação avisa com `answer_type_deprecated`.
- `answer_field`: a chave de um campo personalizado que já existe na conta, em que a resposta é salva, ou `""` para não salvar. A API não cria campos: crie o campo no console e confira a chave em `GET /v1/custom-fields`. Nos blocos seguintes, a resposta está na variável com essa chave, como `{email}`.
- `next`: 1 saída, depois da resposta.

A próxima mensagem de texto do contato responde a pergunta, mesmo que tenha a palavra-chave de outra automação, que então não começa. Uma mensagem sem texto, como um áudio, uma figurinha ou uma imagem sem legenda, não responde: a pergunta continua esperando. No WhatsApp, a legenda de uma imagem ou de um vídeo conta como texto. No Instagram, uma resposta a um story também não responde a pergunta.

### `media`

Envia uma imagem, um vídeo, um áudio ou um documento.

- `upload_id`: opcional, o id de um arquivo enviado ao console da conta.
- `media_url`: opcional, o endereço do arquivo, com variáveis. Só no Instagram.
- `next`: 1 saída.

Envie exatamente um dos dois, `upload_id` ou `media_url`. No WhatsApp, só `upload_id` funciona.

### `buttons`

Envia um texto com até três botões.

- `subtitle`: o texto, de 1 a 4096 caracteres, com variáveis. O Instagram entrega até 1000 caracteres e o WhatsApp, até 1024, e menos quando o plano da conta inclui a assinatura da Gaio.
- `buttons`: de 0 a 3 botões, cada um com:
  - `id`: opcional, um texto que identifica o botão nas métricas. Sem ele, as métricas usam a posição e o título do botão, e mudar um dos dois separa as contagens;
  - `title`: o texto do botão, de 1 a 20 caracteres;
  - `url`: opcional, um endereço `http` ou `https`, que transforma o botão num link. No Instagram, o clique abre o link e segue a saída do botão, mas não abre a janela de 24 horas: um bloco que envia mensagem nessa saída só é entregue se a janela do contato já estiver aberta, e, se não estiver, a execução para nele. No WhatsApp, o clique abre o link, mas a saída do botão ainda não funciona: deixe-a `null`;
  - `whatsapp_connection_id` e `whatsapp_message`: opcionais, transformam o botão num link para conversar com um número de WhatsApp conectado da conta, com a mensagem já escrita (até 300 caracteres; a Gaio põe um código `Ref:` antes dela). Com `whatsapp_connection_id`, a `url` do botão é ignorada. O clique não segue a saída do botão: ela segue quando o contato envia a mensagem para esse número, e a execução continua no WhatsApp. Por isso, os blocos dessa saída precisam funcionar no WhatsApp. Só em automações do Instagram; numa automação do WhatsApp, o botão vira um botão de resposta comum.
- `next`: 4 saídas. As três primeiras seguem o toque em cada botão, na ordem dos botões. A 4ª segue logo depois do envio; com `null`, nada roda ali. Nos dois casos, a execução fica esperando um toque, por até 90 dias.

Enquanto a execução espera um toque, uma mensagem do contato não responde os botões: ela é tratada como uma mensagem nova e pode iniciar outra automação pela palavra-chave.

No WhatsApp, a mensagem tem de 1 a 3 botões de resposta ou exatamente um botão de link, sem misturar os dois.

### `cards`

Envia um carrossel de cards, cada um com imagem e até três botões.

- `caption`: opcional, o texto que acompanha o carrossel, de até 4096 caracteres, com variáveis. O WhatsApp entrega até 1024 caracteres. Obrigatório no WhatsApp; o Instagram ignora.
- `cards`: de 1 a 10 cards, cada um com:
  - `title` e `subtitle`: até 80 caracteres cada, com variáveis;
  - `upload_id`: opcional (obrigatório no WhatsApp), a imagem, um arquivo enviado ao console;
  - `buttons`: de 0 a 3 botões, com os mesmos campos dos botões do bloco [`buttons`](https://gaio.social/developers/automacoes.md#buttons).
- `next`: 31 saídas. O botão `b` do card `c` (os dois contados a partir de 0) segue a saída `c × 3 + b`. A 31ª segue logo depois do envio.

No WhatsApp, o carrossel tem de 2 a 10 cards, todos com imagem e com de 1 a 3 botões de resposta ou exatamente um botão de link, do mesmo tipo em todos os cards.

### `tags`

Põe ou tira etiquetas do contato.

- `operation`: `add` ou `remove`.
- `tags`: os nomes das etiquetas, exatamente como estão na conta (o `name` de `GET /v1/tags`), com as mesmas maiúsculas e acentos. A validação aceita outra grafia do mesmo nome, como `cliente vip` para "Cliente VIP". Mas, com ela, o `add` renomeia a etiqueta da conta, e o `remove` não tira a etiqueta do contato.
- `next`: 1 saída.

### `data`

Grava um valor num campo personalizado do contato.

- `name`: a chave do campo, de até 50 caracteres, que precisa existir na conta.
- `value`: opcional, de 1 a 100 caracteres, com variáveis. Também aceita `$input` e `$timestamp` (veja [Variáveis](https://gaio.social/developers/automacoes.md#variáveis)). Sem `value`, o bloco apaga o valor do campo no contato.
- `next`: 1 saída.

### `math`

Faz uma conta e grava o resultado num campo personalizado.

- `result_field`: a chave do campo que recebe o resultado, de até 50 caracteres, que precisa existir na conta.
- `operand_a` e `operand_b`: `{ "type": "field", "value": "<chave do campo>" }`, com um campo que existe na conta, ou `{ "type": "constant", "value": "10.5" }`. O `value` é sempre um texto, inclusive na constante: `10`, sem aspas, é recusado com `schema_violation`. Na constante, use ponto como separador decimal: `"1.234"` é lido como 1,234.
- `operator`:
  - `add`, `sub`, `mul` e `div`: as quatro operações;
  - `min` e `max`: o menor e o maior dos dois;
  - `mod`: o resto da divisão;
  - `pct_of`: `a` por cento de `b` (`a × b ÷ 100`);
  - `pct_add`: `a` acrescido de `b` por cento (`a × (1 + b ÷ 100)`).
- `next`: 1 saída.

Uma divisão ou um `mod` por zero dá `0`. Um campo vazio, ou um campo de texto com um valor que não é só um número com ponto decimal (como `1,5` ou `R$ 10`), conta como `0`. O resultado é gravado como o número escrito com ponto e sem arredondar, como `20`, `10.5` ou `0.3333333333333333`, e é esse texto que a variável do campo mostra nos blocos seguintes.

### `conditional`

Segue uma saída se as condições forem verdadeiras e a outra se não forem.

- `conditions`: a lista de condições, cada uma com `field_type`, `field_name`, `operator` e `value`, conforme a tabela abaixo.
- `combinator`: opcional, `and` (todas as condições, o padrão) ou `or` (pelo menos uma).
- `next`: 2 saídas, a verdadeira e a falsa.

| `field_type` | `field_name` | `operator` | `value` |
| --- | --- | --- | --- |
| `tag` | O nome da etiqueta, exatamente como está na conta, com as mesmas maiúsculas e acentos. Com outra grafia, a validação passa e `has_tag` é sempre falso. | `has_tag`, `not_has_tag` | — |
| `custom_field` | A chave do campo. | `has_field`, `not_has_field`, `field_equals`, `field_not_equals` | O valor comparado, nos dois últimos. |
| `score` | — | `field_equals`, `field_not_equals`, `field_greater_than`, `field_greater_or_equal`, `field_less_than`, `field_less_or_equal`, `field_between` | O número, ou `min,max` em `field_between`. |
| `variable` | O nome da variável, sem chaves, como `system.current_time` ou a chave de um campo. | `has_field`, `not_has_field`, `field_equals`, `field_not_equals`, `field_contains`, `field_not_contains`, `field_greater_than`, `field_greater_or_equal`, `field_less_than`, `field_less_or_equal`, `field_between`, `date_before`, `date_after`, `date_on`, `time_before`, `time_after`, `time_between` | Um texto, um número, `min,max` em `field_between` (com ponto como separador decimal, como `1.5,3`), uma data (`dd/MM/aaaa` ou `aaaa-MM-dd`), uma hora (`HH:mm`) ou `início,fim` em `time_between`, que pode atravessar a meia-noite (`22:00,06:00`). |
| `follower_status` | — | `is_follower`, `not_follower` | — |
| `messaging_window` | — | `window_open`, `window_closed` | — |

`score` é a pontuação do contato. `follower_status` diz se o contato segue a conta, e só funciona no Instagram. `messaging_window` diz se a janela de 24 horas do contato está aberta no canal da execução.

Na condição `variable`, igualdade e "contém" não diferenciam maiúsculas e minúsculas, e o `value` é comparado como está, sem variáveis. As horas são comparadas por minuto: em `time_between`, os dois extremos entram (`09:00,18:00` vale das 09:00 às 18:00:59), e `time_before` e `time_after` não incluem o próprio minuto. Um `value` fora do formato, ou um operador de comparação sem `value`, deixa a condição falsa, sem erro.

Os campos `field_type`, `field_name`, `operator` e `value` direto em `data`, fora de `conditions`, são a forma antiga de uma condição só. Use `conditions`.

### `randomizer`

Divide as execuções entre as saídas na proporção de cada opção. Não é um sorteio: a primeira execução segue a primeira opção, e cada execução seguinte segue a opção que está mais abaixo do seu percentual. Com 50 e 50, as execuções se alternam. Serve para testes A/B.

- `options`: de 2 a 20 opções `{ "id": "…", "percentage": 50 }`, com percentuais que somam 100, nenhum negativo. O `id` é opcional.
- `next`: uma saída por opção, na ordem das opções.

### `sleep`

Espera antes de seguir.

- `seconds`: de 1 a 82800 (23 horas).
- `next`: 1 saída, depois da espera.

Uma mensagem depois da espera só é entregue se a janela de 24 horas do contato ainda estiver aberta (veja [Enviar uma mensagem](https://gaio.social/developers/index.md#enviar-uma-mensagem)). A exceção é a primeira mensagem de uma execução iniciada por um comentário, que sai como resposta privada e não depende da janela (veja [Gatilho](https://gaio.social/developers/automacoes.md#gatilho)).

### `start`

Inicia outra automação para o mesmo contato, no mesmo canal.

- `workflow_id`: o id da outra automação, publicada no mesmo canal (a mesma conexão) em que a execução está. Quando ela não está publicada nesse canal (está pausada, na lixeira ou em outro canal), a execução termina neste bloco, com `parallel` `true` ou `false`. A validação avisa desses casos (veja os avisos em [Erros de validação](https://gaio.social/developers/automacoes.md#erros-de-validação)).
- `parallel`: com `true`, a outra automação começa e esta segue em seguida. Com `false`, esta espera a outra terminar.
- `delay_seconds`: opcional, de 0 a 82800, uma espera antes de iniciar.
- `next`: 1 saída.

### `invoke`

Faz uma requisição HTTP para o seu sistema, como um webhook.

- `method`: o método HTTP, como `POST`.
- `endpoint`: o endereço, `http` ou `https`, com variáveis.
- `headers`: opcional, uma lista de `{ "name": "…", "value": "…" }` (o nome de 1 a 100 caracteres, o valor de 1 a 1000), com variáveis e [segredos](https://gaio.social/developers/automacoes.md#segredos) no `value`.
- `data`: opcional, o corpo da requisição, um texto com variáveis e segredos. Sem cabeçalho `Content-Type`, o corpo vai como `application/json`.
- `fields`: opcional, uma lista de `{ "path": "$.cliente.id", "name": "<chave do campo>" }` (o `path` de 1 a 500 caracteres, o `name` de 1 a 50), que copia valores de uma resposta JSON de sucesso, por [JSONPath](https://goessner.net/articles/JsonPath/), para campos personalizados do contato. Só o primeiro resultado de cada `path` é copiado. Um resultado `0`, `false` ou vazio não é copiado, e o campo fica como estava.
- `next`: 2 saídas. A primeira segue quando a resposta é `2xx`, e a segunda segue em qualquer outro caso.

Cada tentativa espera a resposta por até 60 segundos e segue até 5 redirecionamentos. Uma resposta `429` ou `5xx` é repetida, até 3 tentativas no total, então o seu sistema deve aceitar a mesma requisição mais de uma vez. Endereços de rede privada são recusados e seguem a saída de falha.

Num corpo JSON, as variáveis de texto entram já escapadas, mas sem aspas: escreva as aspas no próprio corpo, como em `"{\"instagram\": \"{contact.alias}\"}"`. O exemplo [Lead para o CRM](https://gaio.social/developers/automacoes.md#lead-para-o-crm) mostra um corpo completo.

### `google-sheets`

Escreve, atualiza ou apaga uma linha numa planilha do Google conectada à conta.

- `google_external_id`, `spreadsheet_id` e `tab_title`: a conta do Google, a planilha e a aba. `spreadsheet_name`, opcional, é o nome da planilha que o editor mostra. A API não lista contas nem planilhas do Google: monte o bloco no editor do console e leia os valores com `GET /v1/workflows/{workflowId}`.
- `operation`:
  - `append`: acrescenta uma linha;
  - `update`: reescreve a primeira linha encontrada inteira. As colunas que não estão em `values` e os valores que saem vazios ficam vazios na planilha;
  - `upsert`: atualiza a primeira linha encontrada, sem apagar uma célula quando o valor sai vazio, ou acrescenta uma linha se nenhuma for encontrada;
  - `delete`: apaga a primeira linha encontrada.
- `match`: opcional, uma lista de `{ "column": "…", "value": "…" }`, com variáveis no `value`, que encontra a primeira linha em que todas as colunas têm exatamente esses valores. É o que `update`, `upsert` e `delete` usam. Sem `match`, ou com uma lista vazia, eles agem na primeira linha de dados da aba: envie sempre um `match` nessas operações.
- `values`: opcional, as colunas e os valores escritos, como `{ "Nome": "{contact.name}", "Data": "{system.current_date}" }`. As colunas são os títulos da linha 1 da aba, e uma coluna que a aba não tem é ignorada. Cada valor é um texto: escreva `"30"`, não `30`, ou o bloco falha na execução.
- `next`: 1 saída.

Os valores aceitam variáveis, `$input` e `$timestamp`, e também `{contact.tag.<etiqueta>}`, que vira `true` ou `false` conforme o contato tenha a etiqueta. A etiqueta aparece em minúsculas, sem acentos e com um `-` no lugar de cada trecho que não é letra nem número, até 50 caracteres: "Cliente VIP" vira `{contact.tag.cliente-vip}`, e "CPF/CNPJ" vira `{contact.tag.cpf-cnpj}`. Se a conta já teve outra etiqueta com o mesmo nome, o identificador pode ter um sufixo, como `cliente-vip-1`. Um valor que começa com `=`, `+`, `-` ou `@` é escrito como texto, nunca como fórmula.

### `multichannel`

Continua a execução com a mesma pessoa em outro canal conectado.

- `mode`: `first` segue pelo primeiro canal da lista em que o contato pode receber mensagens. `all` inicia uma execução em cada canal em que ele pode receber.
- `channels`: de 1 a 10 canais `{ "id": "…", "provider": "whatsapp", "connection_id": "…" }`, sem repetir conexão. `provider` é `instagram` ou `whatsapp`; o `id` é opcional.
- `next`: uma saída por canal, na ordem dos canais, e mais uma, seguida quando nenhum canal pode receber.

### `pause-automations`

Pausa as automações para o contato e encerra a execução.

- `minutes`: por quanto tempo as automações ficam pausadas para o contato, de 1 a 52034400 (cerca de 99 anos).
- `next`: 1 saída, que nunca é seguida. Envie `[null]`.

### `comment`

Uma anotação no editor. Não faz nada.

- `text`: opcional, a anotação.
- `next`: 1 saída.

### `blank`

Um bloco vazio. Não faz nada.

- `next`: opcional, 1 saída.

### `unsupported`

Um passo importado de outra ferramenta que a Gaio não converteu. Não faz nada. Não crie esse bloco; se ele vier numa automação que você leu, devolva como veio.

- `data`: o conteúdo original do passo.
- `next`: 1 saída.

## Variáveis

Os textos dos blocos aceitam variáveis entre chaves, como `{contact.first_name}`, trocadas pelo valor na hora em que o bloco roda. Os campos que aceitam variáveis estão no [catálogo de blocos](https://gaio.social/developers/automacoes.md#catálogo-de-blocos).

- O nome tem só letras sem acento, números, `_`, `.` e `-`, sem espaços. `{ contact.name }` e `{ação}` não são variáveis e ficam no texto como estão.
- Maiúsculas e minúsculas contam: `{Contact.Name}` não é `{contact.name}`.
- Uma variável que não existe vira texto vazio, sem erro. Confira o nome: a validação não confere as variáveis.
- Uma variável que existe, mas está sem valor, como `{contact.picture_url}` no WhatsApp, vira o texto `null`. Antes de usar uma variável que pode faltar, confira com um bloco [`conditional`](https://gaio.social/developers/automacoes.md#conditional) (`has_field` numa condição `variable`).
- Não há como escrever uma palavra entre chaves num campo com variáveis: ela vira o valor ou some.

### Do contato

| Variável | Valor |
| --- | --- |
| `{contact.id}` | O id do contato, o mesmo de `GET /v1/contacts/{contactId}`. |
| `{contact.name}` | O nome do contato no canal. Sem nome, o `contact.alias`. |
| `{contact.first_name}` | O primeiro nome. Sem nome, o `contact.alias`. |
| `{contact.alias}` | No Instagram, o nome de usuário, sem `@`. No WhatsApp, o telefone só com dígitos, com o código do país e sem `+`: o `whatsapp.phone` do contato sem o `+`, como `5511999998888`. Um celular brasileiro pode vir sem o nono dígito, quando é assim que o WhatsApp o identifica. |
| `{contact.score}` | A pontuação do contato. |
| `{contact.is_verified}` | `true` quando a conta do contato no Instagram tem o selo de verificada e `false` em qualquer outro caso, inclusive quando o Instagram ainda não mostrou o perfil e no WhatsApp. |
| `{contact.follower_count}` | Quantos seguidores a conta do contato tem no Instagram, segundo a última leitura do perfil, que a Gaio refaz quando o contato interage e a leitura guardada tem mais de um dia. Vira o texto `null` quando o Instagram ainda não mostrou o perfil e no WhatsApp; numa condição, ele não casa com `field_greater_than`, `field_greater_or_equal`, `field_less_than`, `field_less_or_equal` nem `field_between`, e `not_has_field` separa esse contato. |
| `{contact.picture_url}` | O endereço da foto de perfil. No WhatsApp, e no Instagram quando o perfil não tem foto, vira o texto `null`. |
| `{contact.picture_url_base64}` | O endereço da foto de perfil codificado em Base64. |
| `{contact.tags}` | Os nomes das etiquetas do contato, separados por vírgula e espaço, como `cliente, vip`. A ordem é a do código dos caracteres: maiúsculas antes de minúsculas e letras acentuadas no fim. Já inclui as etiquetas que a própria execução pôs ou tirou. |

O Instagram mostra o perfil de um contato, com o selo e os seguidores, a partir da primeira mensagem que ele manda para a conta, ou do primeiro toque num ice-breaker. Quem só comentou ainda não tem perfil.

### Da mensagem

| Variável | Valor |
| --- | --- |
| `{message.trigger}` | O texto da mensagem ou do comentário que iniciou a automação. Vazio quando a execução começou pela API, por um envio em massa ou por uma menção. Numa automação iniciada por um bloco `start`, é o texto do gatilho da automação que a iniciou. |
| `{message.last_contact}` | O texto da última mensagem do contato. Quando essa mensagem não tem texto (um áudio, uma imagem) ou o contato nunca escreveu, vira o texto `null`. Numa automação iniciada por `message_sent`, até a execução parar para esperar (uma pergunta, um botão ou um `sleep` de mais de 5 segundos), é o texto que a própria conta enviou. |
| `{message.last_sender}` | Quem mandou a última mensagem da conversa: `contact`, o contato, ou `user`, a conta (pela equipe ou pela automação). |
| `{message.interaction_type}` | O tipo da interação que trouxe a execução até o bloco: `dm`, `dm_sent`, `comment`, `story`, `live` ou `mention`. No começo, é a que iniciou a automação. Depois da resposta a um `question`, é `dm` (ou `story`), mesmo numa automação iniciada por comentário. Depois de um botão, e numa execução iniciada por um envio em massa, é o tipo da última mensagem do contato. Vazio quando a execução começou pela API. |
| `{message.post_id}` | O id do post comentado, quando a automação começou por um comentário. |
| `{message.post_url}` | O link desse post, quando a Gaio o conhece. |
| `{message.collaborator_email}` | Numa automação iniciada por `message_sent`, o e-mail de quem da equipe digitou a mensagem no chat ou na caixa de entrada do console: o e-mail do perfil do dono da conta ou de um colaborador. Vazio quando a mensagem foi enviada pelo app do Instagram e em qualquer outro gatilho. Numa automação iniciada por um bloco `start`, é o da automação que a iniciou. |

### Da conversa

`{conversation.last_30m}`, `{conversation.last_1h}` e `{conversation.last_24h}` trazem a conversa da janela, da mensagem mais antiga para a mais recente, uma por linha: `[2026-10-05 14:30:00] Contact: …` ou `[2026-10-05 14:31:12] Agent: …`. São até 50, 75 e 100 mensagens, e as mensagens sem texto ficam de fora. Os horários estão em UTC, e não no fuso da conta. `Agent` é qualquer mensagem que não veio do contato, seja da equipe ou da automação.

### Do sistema

`{system.current_date}` (`dd/MM/aaaa`), `{system.current_time}` (`HH:mm:ss`) e `{system.current_datetime}` (`dd/MM/aaaa HH:mm:ss`), no fuso horário da conta, definido no console em **Configurações › Conta › Geral** (o padrão é o de São Paulo). A API não informa o fuso.

### Campos personalizados

Cada campo personalizado é uma variável pela chave dele, o `name` de `GET /v1/custom-fields`, e não pelo rótulo: o campo "Maior dificuldade" é `{maior-dificuldade}`. A chave nasce do rótulo, quando o campo é criado no console, em minúsculas, sem acentos e com um `-` no lugar de cada trecho que não é letra nem número, e não muda quando o rótulo muda. Se a conta já teve um campo com a mesma chave, a nova ganha um sufixo, como `cidade-1`: confira sempre em `GET /v1/custom-fields`.

O valor que a própria execução grava (por `data`, pela resposta de um `question`, por `math` ou pelos `fields` de um `invoke`) já vale nos blocos seguintes. Use a chave do campo nesses blocos, para que a variável e o campo salvo tenham o mesmo nome.

### Da conta

`{account.<nome>}` é uma variável de texto que a conta define no console.

### Segredos

`{secret.<nome>}` é um segredo da conta, como um token do seu sistema, cadastrado no console com o domínio em que pode ser usado. Ele só funciona nos `headers` e no `data` de um [`invoke`](https://gaio.social/developers/automacoes.md#invoke), e só quando o endereço da requisição é exatamente desse domínio. Em qualquer outro lugar, o segredo vira texto vazio, e a requisição sai sem a credencial.

### `$input` e `$timestamp`

No `value` de um [`data`](https://gaio.social/developers/automacoes.md#data) e, num [`google-sheets`](https://gaio.social/developers/automacoes.md#google-sheets), nos `values` e no `value` de cada `match`, o valor inteiro do campo pode ser:

- `$input`: o texto que chegou com a mensagem atual, como a resposta a um `question` ou o texto do gatilho;
- `$timestamp`: o momento atual, em milissegundos desde 1º de janeiro de 1970 (UTC).

Só funciona quando o campo tem exatamente `$input` ou `$timestamp`. Por isso, qualquer valor desses campos que comece com `$` é lido como variável: `"$50"` vira vazio.

## O catálogo em JSON Schema

`GET /v1/workflow-graph-schema` (`workflows:read`) responde o catálogo de blocos em JSON Schema (2020-12) e a versão dele:

```json
{
  "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "…": "…" },
  "catalog_version": "…"
}
```

O `schema` descreve `graph.nodes`: cada tipo de bloco, os campos de `data`, os limites e o formato dos ids. Com ele, você confere um grafo antes de enviá-lo ou gera tipos para o seu código. A [cópia do catálogo](https://gaio.social/developers/workflow-graph-schema.json) traz o mesmo `schema`, com a versão no `$comment`, para consultar sem chave; a resposta da API é a referência.

O JSON Schema não expressa tudo. A validação confere: o tipo do `root`, a quantidade de saídas, os blocos citados em `next`, o limite de 256 KiB e os recursos da conta. Confere também as regras do compilador: de 2 a 20 opções num `randomizer`, com percentuais que somam 100 e nenhum negativo; `mode` e de 1 a 10 canais num `multichannel`; exatamente uma origem num `media`; condições completas num `conditional`. Por último, as regras de publicação. O limite de 500 blocos está no `maxProperties` do schema.

O catálogo evolui fora da versão da API e cresce por adição: um tipo de bloco, um campo opcional, um valor. Guarde o `catalog_version` para notar quando ele muda. Uma mudança que recusaria um grafo aceito hoje é anunciada com 90 dias de antecedência, e, nesse prazo, a validação avisa sobre os grafos afetados.

## Exemplos

Os exemplos do Instagram, a boas-vindas inclusive, precisam de uma conta do Instagram conectada na sua conta da Gaio. Os exemplos também usam recursos que a sua conta precisa ter: as etiquetas `lead-instagram`, `orcamento` e `cliente`, o campo personalizado `email`, um número de WhatsApp conectado no lugar de `<id-da-conexao-whatsapp>` e, no último, um segredo `crm_token` para o domínio `api.exemplo.com.br`. Cada exemplo tem o arquivo para baixar.

### Cupom por comentário

Quem comenta "cupom" num post recebe o cupom com dois botões. "Ver a loja" abre o link e etiqueta o contato como lead; "Falar com a gente" avisa que alguém do time vai responder. [cupom-por-comentario.json](https://gaio.social/developers/exemplos/cupom-por-comentario.json)

```json
{
  "title": "Cupom por comentário",
  "channel": { "type": "instagram" },
  "trigger": { "events": ["comment"], "match": "contains", "keywords": ["cupom"] },
  "graph": {
    "nodes": {
      "root": { "type": "root", "data": { "next": ["oferta"] } },
      "oferta": {
        "type": "buttons",
        "data": {
          "subtitle": "Seu cupom é BEMVINDO10. Quer ver a loja ou falar com a gente?",
          "buttons": [
            { "id": "ver_loja", "title": "Ver a loja", "url": "https://exemplo.com.br/loja" },
            { "id": "falar", "title": "Falar com a gente" }
          ],
          "next": ["marcar_lead", "atendimento", null, null]
        }
      },
      "marcar_lead": {
        "type": "tags",
        "data": { "operation": "add", "tags": ["lead-instagram"], "next": [null] }
      },
      "atendimento": {
        "type": "message",
        "data": { "text": "Já chamamos alguém do time para falar com você.", "next": [null] }
      }
    }
  }
}
```

O terceiro `null` é a saída do terceiro botão, que não existe, e o quarto é a saída depois do envio.

### Pedido de orçamento no WhatsApp

Quem manda "orçamento" no WhatsApp recebe uma pergunta, e a resposta vai para o campo `email`. Depois, a automação confirma e etiqueta o contato. [orcamento-whatsapp.json](https://gaio.social/developers/exemplos/orcamento-whatsapp.json)

```json
{
  "title": "Pedido de orçamento",
  "channel": { "type": "whatsapp", "connection_id": "<id-da-conexao-whatsapp>" },
  "trigger": { "events": ["message"], "match": "contains", "keywords": ["orçamento"] },
  "graph": {
    "nodes": {
      "root": { "type": "root", "data": { "next": ["pedir_email"] } },
      "pedir_email": {
        "type": "question",
        "data": {
          "text": "Para qual e-mail mandamos o orçamento?",
          "answer_type": "any",
          "answer_field": "email",
          "next": ["confirmar"]
        }
      },
      "confirmar": {
        "type": "message",
        "data": { "text": "Obrigado! Você recebe o orçamento em até 1 dia útil.", "next": ["etiquetar"] }
      },
      "etiquetar": {
        "type": "tags",
        "data": { "operation": "add", "tags": ["orcamento"], "next": [null] }
      }
    }
  }
}
```

### Preço para cliente e para novo contato

Quem pergunta "preço" recebe um valor se já tem a etiqueta `cliente` e outro se não tem. O novo contato recebe um lembrete uma hora depois. [cliente-ou-novo.json](https://gaio.social/developers/exemplos/cliente-ou-novo.json)

```json
{
  "title": "Preço para cliente e para novo contato",
  "channel": { "type": "instagram" },
  "trigger": { "events": ["message"], "match": "contains", "keywords": ["preço"] },
  "graph": {
    "nodes": {
      "root": { "type": "root", "data": { "next": ["ja_e_cliente"] } },
      "ja_e_cliente": {
        "type": "conditional",
        "data": {
          "conditions": [{ "field_type": "tag", "field_name": "cliente", "operator": "has_tag" }],
          "combinator": "and",
          "next": ["preco_cliente", "preco_novo"]
        }
      },
      "preco_cliente": {
        "type": "message",
        "data": { "text": "Como você já é cliente, o plano sai por R$ 89 por mês.", "next": [null] }
      },
      "preco_novo": {
        "type": "message",
        "data": { "text": "O plano custa R$ 99 por mês, com 7 dias grátis.", "next": ["esperar"] }
      },
      "esperar": { "type": "sleep", "data": { "seconds": 3600, "next": ["lembrete"] } },
      "lembrete": {
        "type": "message",
        "data": { "text": "Ficou alguma dúvida sobre o plano? É só responder aqui.", "next": [null] }
      }
    }
  }
}
```

### Teste A/B da mensagem de abertura

Quem responde um story recebe uma de duas mensagens, meio a meio: as execuções se alternam entre as duas. [teste-a-b.json](https://gaio.social/developers/exemplos/teste-a-b.json)

```json
{
  "title": "Teste A/B da mensagem de abertura",
  "channel": { "type": "instagram" },
  "trigger": { "events": ["story_message"], "match": "any" },
  "graph": {
    "nodes": {
      "root": { "type": "root", "data": { "next": ["sortear"] } },
      "sortear": {
        "type": "randomizer",
        "data": {
          "options": [
            { "id": "versao_a", "percentage": 50 },
            { "id": "versao_b", "percentage": 50 }
          ],
          "next": ["mensagem_a", "mensagem_b"]
        }
      },
      "mensagem_a": {
        "type": "message",
        "data": { "text": "Obrigado por responder o story! Quer conhecer a novidade?", "next": [null] }
      },
      "mensagem_b": {
        "type": "message",
        "data": { "text": "Vimos sua resposta. Temos uma novidade para você!", "next": [null] }
      }
    }
  }
}
```

### Lead para o CRM

Quem manda "quero" informa o e-mail, e a automação envia o contato para o seu CRM, com o token num segredo da conta. Se o CRM responder com erro, o contato é avisado. [lead-para-o-crm.json](https://gaio.social/developers/exemplos/lead-para-o-crm.json)

```json
{
  "title": "Lead para o CRM",
  "channel": { "type": "instagram" },
  "trigger": { "events": ["message"], "match": "contains", "keywords": ["quero"] },
  "graph": {
    "nodes": {
      "root": { "type": "root", "data": { "next": ["pedir_email"] } },
      "pedir_email": {
        "type": "question",
        "data": {
          "text": "Qual é o seu e-mail? Um especialista vai falar com você.",
          "answer_type": "any",
          "answer_field": "email",
          "next": ["enviar_crm"]
        }
      },
      "enviar_crm": {
        "type": "invoke",
        "data": {
          "method": "POST",
          "endpoint": "https://api.exemplo.com.br/leads",
          "headers": [{ "name": "Authorization", "value": "Bearer {secret.crm_token}" }],
          "data": "{\"instagram\": \"{contact.alias}\", \"nome\": \"{contact.name}\", \"email\": \"{email}\", \"mensagem\": \"{message.trigger}\"}",
          "next": ["confirmar", "avisar_falha"]
        }
      },
      "confirmar": {
        "type": "message",
        "data": { "text": "Pronto, {contact.first_name}! Recebemos o seu contato.", "next": [null] }
      },
      "avisar_falha": {
        "type": "message",
        "data": {
          "text": "Não conseguimos registrar agora. Pode mandar o seu e-mail de novo daqui a pouco?",
          "next": [null]
        }
      }
    }
  }
}
```

A [boas-vindas](https://gaio.social/developers/exemplos/boas-vindas.json) do início da página também tem arquivo.

## Validar, criar e publicar

Os passos, com os escopos de cada um:

1. **Validar a definição** (`workflows:read`). `POST /v1/workflows/validate` recebe só `channel`, `trigger` e `graph`, os três obrigatórios (com `title`, `description` ou `folder_id`, a resposta é `400`), e não salva nada:

   ```bash
   jq '{channel, trigger, graph}' boas-vindas.json |
     curl https://api.gaio.social/v1/workflows/validate \
       -H "Authorization: Bearer $GAIO_API_KEY" \
       -H "Content-Type: application/json" \
       -d @-
   ```

   ```json
   { "valid": true, "errors": [], "warnings": [] }
   ```

   `valid` é `true` quando `errors` está vazio. Nesse caso, a publicação não recusa a automação com `workflow_invalid`, mas o motor de automações ainda pode recusar a automação compilada, com `422` e o código `workflow_not_publishable` (o motivo vem em `context.details`). Os `warnings` não impedem a publicação, mas apontam algo que provavelmente não faz o que você queria. Validar conta como leitura no [limite de uso](https://gaio.social/developers/index.md#limites-de-uso).

2. **Criar** (`workflows:write`). `POST /v1/workflows` recebe a definição inteira e cria um rascunho:

   ```bash
   curl https://api.gaio.social/v1/workflows \
     -H "Authorization: Bearer $GAIO_API_KEY" \
     -H "Content-Type: application/json" \
     -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
     -d @boas-vindas.json
   ```

   A resposta, `201`, é a automação, com `status: "draft"` e o cabeçalho `ETag: "0"`. A criação confere o grafo, o canal e o gatilho, mas não tudo o que a publicação confere: valide antes de criar.

3. **Publicar** (`workflows:publish`). `POST /v1/workflows/{workflowId}/publish`, com o `ETag` da última resposta em `If-Match`, publica exatamente aquela revisão:

   ```bash
   curl https://api.gaio.social/v1/workflows/$WORKFLOW_ID/publish \
     -X POST \
     -H "Authorization: Bearer $GAIO_API_KEY" \
     -H 'If-Match: "0"'
   ```

   A resposta, `200`, traz `status: "published"`. Daí em diante, a automação responde os seus contatos.

Para mudar uma automação, leia com `GET /v1/workflows/{workflowId}`, altere e envie com `PATCH`, com o `ETag` da leitura em `If-Match` (veja [Editar e publicar uma automação](https://gaio.social/developers/index.md#editar-e-publicar-uma-automação)). No `PATCH`, `graph` substitui o grafo inteiro, e um bloco que ficou de fora é apagado; `trigger` também substitui o gatilho inteiro. Antes de publicar de novo, `POST /v1/workflows/{workflowId}/validate` confere o rascunho salvo e responde a `revision` conferida.

## Erros de validação

Cada problema vem com o código, o bloco e o caminho do campo:

```json
{
  "valid": false,
  "errors": [
    {
      "code": "next_arity_invalid",
      "message": "A buttons node takes exactly 4 next entries (null for a branch that goes nowhere), got 2.",
      "node_id": "oferta",
      "path": "nodes.oferta.data.next"
    }
  ],
  "warnings": []
}
```

Os erros, que impedem a publicação:

| Código | O que está errado |
| --- | --- |
| `schema_violation` | Um bloco não segue o catálogo: um tipo desconhecido, um campo que falta, sobra ou tem o formato errado. |
| `graph_too_large` | Mais de 500 blocos ou mais de 256 KiB. |
| `node_id_invalid` | Um id fora do formato. |
| `root_missing` | Falta o bloco `root` do tipo `root`. |
| `next_arity_invalid` | Um `next` com mais ou menos entradas do que as saídas do bloco. |
| `node_not_found` | Um `next` aponta para um bloco que não existe. |
| `tag_not_found`, `custom_field_not_found`, `upload_not_found` | O bloco cita uma etiqueta, um campo personalizado ou um upload que a conta não tem. |
| `link_button_url_empty` | Um botão com `url` vazia. |
| `media_source_invalid` | Um `media` sem `upload_id` e sem `media_url`, ou com os dois. |
| `randomizer_options_invalid` | Um `randomizer` com menos de 2 ou mais de 20 opções, com um percentual negativo ou com percentuais que não somam 100. |
| `conditional_incomplete` | Uma condição sem `field_type`, `operator`, `field_name` ou `value`, quando eles são necessários. |
| `multichannel_mode_missing`, `multichannel_channels_invalid` | Um `multichannel` sem `mode`, sem canais ou com mais de 10, com um canal de `connection_id` vazio ou com a mesma conexão repetida. |
| `handoff_number_disconnected` | Um botão leva para um número de WhatsApp que não está conectado. O número vem em `connection_id`. |
| `channel_required`, `channel_not_found`, `channel_type_mismatch` | O canal falta, não existe na conta ou é de outro tipo. |
| `posts_without_event`, `next_post_without_comment`, `next_post_with_posts` | O gatilho tem posts sem o evento que eles limitam, ou `next_post` sem `comment` ou junto com posts do feed. |
| `replies_list_not_supported`, `replies_list_not_found` | A lista de respostas está numa automação do WhatsApp ou não existe na conta. |

Os avisos, que não impedem a publicação:

| Código | O que provavelmente não funciona |
| --- | --- |
| `keyword_collision` | Outra automação publicada no mesmo canal responde ao mesmo gatilho. O aviso traz `workflow_ids` e `keywords`. |
| `trigger_event_not_supported` | Um evento que o canal não tem, como `comment` no WhatsApp. Ele nunca dispara. |
| `trigger_has_no_keywords` | Um `match` que pede palavras-chave sem nenhuma palavra-chave. A automação nunca dispara. |
| `start_workflow_not_found` | Um `start` aponta para uma automação que a conta não tem, nem na lixeira. A execução termina no bloco. O id vem em `workflow_ids`. |
| `start_workflow_deleted` | Um `start` aponta para uma automação que está na lixeira. A execução termina no bloco. O id vem em `workflow_ids`, e o id e o título, em `workflows`. |
| `start_workflow_unpublished` | Um `start` aponta para uma automação que não está publicada. Enquanto ela não for publicada, a execução termina no bloco. O id vem em `workflow_ids`, e o id e o título, em `workflows`. |
| `start_workflow_other_channel` | Um `start` aponta para uma automação publicada em outro canal. Uma automação só inicia outra do mesmo canal, e a execução termina no bloco. O id vem em `workflow_ids`, e o id e o título, em `workflows`. |
| `answer_type_deprecated` | Um `question` com `answer_type: "number"`, que roda como `any`: a resposta não é conferida. Troque por `any`. |
| `next_arity_invalid` | Num rascunho salvo pelo editor do console, um `next` com mais ou menos entradas do que as saídas. Ao ler, a API completa com `null` o que falta, mas não corta o que sobra: corte a sobra antes de reenviar o grafo, ou o `PATCH` responde `invalid_graph`. |

Criar e alterar conferem só parte da tabela de erros:

- um problema de formato, tamanho, ids, `root`, saídas ou recursos da conta (`schema_violation`, `graph_too_large`, `node_id_invalid`, `root_missing`, `next_arity_invalid`, `node_not_found`, `tag_not_found`, `custom_field_not_found` e `upload_not_found`) recebe `422`, com o código `invalid_graph` e a lista em `context.errors`;
- um problema do canal recebe `422`, com `invalid_channel` e `context.reason` igual a `required`, `not_found` ou `type_mismatch`;
- um problema do gatilho recebe `422`, com `invalid_trigger` e o código da tabela em `context.reason`, um de cada vez.

Os outros só aparecem na validação e na publicação. Publicar responde `422`, com `workflow_invalid` e os mesmos `errors` da validação, ou com `workflow_not_publishable` (veja o passo 1 de [Validar, criar e publicar](https://gaio.social/developers/automacoes.md#validar-criar-e-publicar)).

A validação confere em etapas. Com `graph_too_large`, `schema_violation`, `node_id_invalid` ou `root_missing`, ela não confere as saídas, os recursos da conta nem as regras de publicação. Em `POST /v1/workflows/validate`, um problema no canal também esconde as regras de publicação. Os erros do compilador (`media_source_invalid`, `randomizer_options_invalid`, `conditional_incomplete`, `multichannel_mode_missing` e `multichannel_channels_invalid`) só aparecem quando o grafo não tem nenhum outro erro de formato, de saídas ou de recursos. Eles vêm um de cada vez, do primeiro bloco com problema no caminho a partir do `root`. Um bloco que não se alcança a partir do `root` não passa por eles. Valide de novo depois de cada correção.

Os `schema_violation` de um bloco falam só do tipo que o `type` dele diz. Um `type` que o catálogo não tem recebe um único `schema_violation`, no caminho `nodes.<id>.type`.

O que está fora do formato do corpo, como um campo desconhecido fora do grafo, recebe `400`, antes da validação (veja [Erros](https://gaio.social/developers/index.md#erros)).

### O que a validação ainda não confere

Estas regras dependem do canal, e a validação não as confere:

- No WhatsApp, os botões de [`buttons`](https://gaio.social/developers/automacoes.md#buttons) e de [`cards`](https://gaio.social/developers/automacoes.md#cards) fora das regras do canal, um carrossel fora de 2 a 10 cards com imagem e legenda e o `media_url` do [`media`](https://gaio.social/developers/automacoes.md#media) fazem o passo falhar quando a automação roda.
- Numa automação do WhatsApp, um botão com `whatsapp_connection_id` vira um botão de resposta comum e não leva para o outro número.
- Numa automação do WhatsApp, uma condição `follower_status` é sempre falsa, com `is_follower` ou com `not_follower`.
- No Instagram, os textos acima de 1000 caracteres são cortados.

Também não são conferidos: se a conexão do canal está conectada (uma conexão desconectada, mas não removida, passa na validação e na publicação, e a automação não responde enquanto ela estiver desconectada), os mínimos de 1 caractere dos textos, as variáveis, a automação de um `start` (só um aviso), as conexões de um `multichannel` e a planilha de um `google-sheets`. Confira esses pontos antes de publicar.

## Pastas

As pastas organizam as automações da conta, como no console. Uma pasta pode ficar dentro de outra, e duas pastas podem ter o mesmo nome. Ler as pastas pede `workflows:read`; criar, alterar e excluir, `workflows:write`.

```bash
curl https://api.gaio.social/v1/folders \
  -H "Authorization: Bearer $GAIO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -d '{"name": "Black Friday"}'
```

A resposta, `201`, é a pasta:

```json
{
  "id": "…",
  "type": "workflow",
  "name": "Black Friday",
  "parent_id": null,
  "created_at": "…",
  "updated_at": "…"
}
```

- **Nome**: de 1 a 128 caracteres, sem os espaços das pontas. Um nome em branco recebe `422`, com `invalid_name`.
- **Pôr uma automação numa pasta**: envie `folder_id` ao criar a automação ou num `PATCH` (com `null`, ela sai da pasta). Mudar de pasta muda a `revision`, mas não o que está rodando: uma automação publicada continua sem alterações para publicar. `GET /v1/workflows?folder_id=…` lista as automações que estão direto naquela pasta, sem as das subpastas.
- **Subpastas**: `parent_id` é a pasta de cima, ou `null` fora de pastas. `GET /v1/folders` lista as pastas de todos os níveis, das mais antigas para as mais novas.
- **Renomear e mover**: `PATCH /v1/folders/{folderId}` com `name`, `parent_id` ou os dois. As subpastas e as automações vão junto. Uma pasta não vai para dentro dela mesma nem de uma subpasta dela (`invalid_parent`).
- **Excluir**: `DELETE /v1/folders/{folderId}` exclui a pasta e as subpastas, como no console. As automações delas não são excluídas: ficam fora de pastas, e o que está rodando não muda.
- **Pasta que não existe**: um `folder_id` (na automação) ou um `parent_id` (na pasta) que não é uma pasta de automações da conta recebe `422`, com `folder_not_found` e o campo em `context.field`.
- **`type`** é o que a pasta guarda: `workflow`, o padrão em `POST /v1/folders` e no filtro `type` de `GET /v1/folders`. Hoje é o único valor, mas trate um valor que você não conhece: outros tipos de pasta podem chegar.

## Listas de respostas

Uma lista de respostas é o que uma automação do Instagram publica embaixo de cada comentário que a inicia: uma das respostas da lista, sorteada a cada comentário, com as variáveis do contato e da conta preenchidas (`{contact.first_name}`, a chave de um campo personalizado). Só os comentários num post ou reel (`comment`) recebem a resposta; um comentário numa live, não. Quando a automação envia mensagem, a resposta pública sai depois que o Instagram aceita a primeira mensagem; se ela for recusada, ou se a execução seguir um caminho sem mensagem, o comentário fica sem resposta pública.

A automação usa a lista pelo `replies_list_id` do [gatilho](https://gaio.social/developers/automacoes.md#gatilho). Só as listas da própria conta servem: uma lista modelo de outra conta não pode ser usada no gatilho. Ler as listas pede `workflows:read`; criar, alterar e excluir, `workflows:write`.

```bash
curl https://api.gaio.social/v1/replies-lists \
  -H "Authorization: Bearer $GAIO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -d '{"name": "Cupom no direct", "replies": ["Te mandei no direct, {contact.first_name}!", "Confere o seu direct 😉"]}'
```

A resposta, `201`, é a lista, com `id`, `name`, `replies`, `created_at` e `updated_at`.

- **Nome**: de 1 a 128 caracteres, sem os espaços das pontas (em branco: `invalid_name`). Único entre as listas da conta, sem diferenciar maiúsculas de minúsculas (`replies_list_already_exists`, com o id da lista que já tem o nome). Uma lista pode trocar o próprio nome só nas maiúsculas.
- **Respostas**: de 1 a 250, cada uma de 1 a 512 caracteres, guardadas como foram escritas e nenhuma em branco (`invalid_reply`, com a posição em `index`).
- **Alterar**: `PATCH /v1/replies-lists/{repliesListId}` com `name`, `replies` ou os dois; `replies` substitui todas as respostas. As respostas novas valem na hora: as automações publicadas que usam a lista passam a publicá-las no próximo comentário, sem publicar a automação de novo.
- **Listar**: `GET /v1/replies-lists` traz as listas com `reply_count`, sem as respostas. Leia uma lista para ter as respostas.
- **Excluir**: `DELETE /v1/replies-lists/{repliesListId}` recusa uma lista que uma automação da conta usa, em rascunho ou publicada (`replies_list_in_use`, com até 50 automações em `workflow_ids` e o total em `workflow_count`). Os modelos e as automações excluídas não contam. Tire a lista do gatilho delas antes.
- **Motor indisponível**: se o motor de automações não responder, nada é salvo e a resposta é `503`, com `service_unavailable`. Tente de novo.
