graphopcional | object | O desenho: `start` (o primeiro nó) e `nodes` (até 50, sem ciclos, todos alcançáveis do início). Cada nó tem
`id` (letras, dígitos, `-` e `_`, até 40) e `type`:
- `trigger`: `{ trigger, filter, next }`, um gatilho de `GET /api/workflows/triggers` com o filtro dele (o mesmo de
`triggerFilter`). **Um por desenho** (fase 5, David 2026-10-26: um segundo dá 422 `code: singleTriggerOnly`); nenhum nó
aponta para ele. Com o nó de gatilho, `start` é ele (o valor enviado é ignorado), e o `triggerType`/`triggerFilter` do
workflow o espelham e só mudam pelo desenho (422 `workflows.triggerInGraph`). Desenho sem nó de gatilho continua usando
`triggerType`/`triggerFilter` como antes;
- `wait`: `{ amount, unit: minutes|hours|days, next }`, de 5 minutos a 90 dias;
- `condition`: `{ match: all|any, rules, then, else }`, cada regra `{ kind, not?, … }` lida do banco na hora do passo:
`hasTag {tag}` ou `hasTag {tags: [...], mode: any|all}` (até 10 tags; `any` é o padrão; `tag` traz a primeira),
`inGroup {groupId}`, `fieldEquals {fieldKey, value}`, `lastSeenOlderThan {days}`,
`completedCourse {courseId}`, `hasActiveCourse {courseId?}`, `purchasedProduct {productId}`, `hasAbandonedCart {productId?}`,
`hasSpaceAccess {spaceId}` (a pessoa abre o espaço hoje, pelas mesmas regras de acesso do app), `visitedSpace {spaceId}` (já entrou nele),
`registeredEvent {eventId}` (tem ingresso ativo) e `attendedEvent {eventId}` (teve presença registrada);
- `sendEmail`: `{ emailId, next }`, um e-mail da biblioteca (uso livre). Cada pessoa recebe a versão no idioma
dela, senão a principal; o passo guarda o assunto e o HTML enviados.
- `sendDm`: `{ fromMemberId, text, next }` (fase 3): DM em nome de uma pessoa da equipe permitida pela comunidade (fase 5g:
`GET /api/workflows/senders`; com "só o owner", outra pessoa dá 422 `senderNotAllowed` ao salvar e o passo é pulado com esse motivo);
respeita bloqueio e Mensagens desligada; no máximo 1 DM de workflow por pessoa por dia. `text` até 1000, com `{{nome}}` e `{{comunidade}}`;
- `comment`: `{ fromMemberId, text, next }` e `react`: `{ fromMemberId, reaction, next }` (🎉 ❤️ 👍 🔥 🙌 😊 👏): agem sobre o item do
gatilho, só com `post.created`, `comment.created`, `reaction.added` ou `event.registered`;
- `tag`: `{ action: add|remove, tag, next }` e `addToGroup`: `{ groupId, next }`: os fatos que geram nunca disparam outro workflow;
- `slackPost`: `{ text, next }` (até 500, `{{nome}}` e `{{comunidade}}`): posta no canal da instalação Slack da comunidade;
- `sendSms`: `{ text, next, error }` (até 300, com as variáveis da 5b, valores em texto puro): SMS de serviço à pessoa da
execução, categoria `workflows`, com `[Comunidade] ` na frente e o link de saída no fim (conte as partes em `POST /api/sms/segments`;
o resto do nó em `smsNode` de `GET /api/workflows/triggers`). Pulado por "ok" com `smsNoConsent`, `smsNoPhone`, `smsPhoneUnverified`,
`smsOptedOut`, `smsCountryUnsupported`, `smsCategoryOff`, `smsChannelDisabled`, `smsTextEmpty`; adiado (`DEFERRED`) por
`smsQuietHours` (21h às 8h no fuso da comunidade), `smsDailyCapReached` e `smsMonthlyQuotaReached`; recusa do envio vai por "erro"
(`smsSendFailed`);
- `sendWhatsapp` (só modelo desde 2026-10-27): `{ template, variables, link, next, error }`. `template` é um modelo
aprovado do catálogo (`whatsappNode.templates`, padrão `communityNoticeV2` = `memberfy_aviso_comunidade_v2`; fora dele os modelos que nenhum gatilho preenche: visitante, encomenda, cartão, chamado, cadastro); `variables` preenche
as variáveis do modelo (até 300 cada, com as variáveis da 5b; primeiro nome e comunidade vêm sozinhos); `link` é o caminho da
comunidade que o botão abre (padrão `/`). Nunca texto livre, nem dentro da janela de 24 h. Um nó antigo com `text` vale como o
aviso da comunidade com esse texto. Pulado por "ok" com os motivos do SMS trocando `sms` por `whatsapp` (`whatsappNoConsent`...),
`whatsappNotOnPlan` e `whatsappTemplateNotApproved`; adiado por `whatsappQuietHours`, `whatsappDailyCapReached`,
`whatsappMonthlyQuotaReached` e `whatsappPersonDailyCapReached`; recusa do envio vai por "erro" (`whatsappSendFailed`);
- `http`: `{ method: GET|POST|PUT|PATCH|DELETE, url, headers: [{ name, value }], payload, timeoutSeconds, next, error }`. `url` https na
porta 443, domínio fixo (variáveis só no caminho e nos parâmetros), nunca local nem de rede privada; até 20 cabeçalhos (os
de transporte, como `Host`, `Content-Type`, `User-Agent` e `X-Memberfy-Run-Id`, são da Memberfy); `payload` (o corpo) é um JSON (até 16 KB,
nunca em GET), que sempre leva `memberfy: { memberLanguage, communityLanguage }`. **`{{env.NOME}}`** (um segredo do cofre,
`GET /api/vault`) só em cabeçalho e corpo, e só para os domínios que o segredo permite; nome desconhecido ou domínio fora
da lista dão 422 ao salvar. Sem o aceite do aviso de dados pessoais (`POST /api/vault/data-notice`), o nó não salva
(`dataNoticeRequired`). Na execução: 2xx segue por "ok" e a resposta JSON vira `{{steps.<nó>.status}}`, `.duration`, `.size` e
`{{steps.<nó>.body.<caminho>}}` (até 16 KB, guardada cifrada); 3xx (`httpRedirect`), 4xx (`httpClientError`), endereço barrado
(`httpUrlBlocked`), resposta acima de 64 KB (`httpResponseTooLarge`) e corpo que não vira JSON (`httpBodyInvalid`) vão para a
saída de erro. `timeoutSeconds` (2 a 15, padrão 10; fora disso, 422 `httpTimeoutInvalid`) é quanto se espera a resposta;
5xx, tempo esgotado ou rede tentam de novo em 1, 5 e 15 minutos e então `httpUnavailable` (o tempo esgotado conta para o
disjuntor). No máximo 20 chamadas ao mesmo tempo por processo e 5 por comunidade: a que passa espera alguns segundos
(`DEFERRED`, `httpBusy`). Nos planos Business e Scale (`workflows.httpFixedIp`) a chamada sempre sai pelos IPs fixos
(`GET /api/vault`, `outbound.ips`), até `workflows.httpFixedIpCallsPerMonth` (10.000 por mês UTC por padrão); passado o
teto, sai por IP dinâmico até o fim do mês e o histórico marca `fixedIpFallback: monthlyCap`. Limites
de 60 por minuto e 2.000 por dia por workflow e 300 por minuto e 20.000 por dia por comunidade, e um disjuntor por domínio
(5 falhas seguidas fecham por 5 minutos): o passo fica `DEFERRED` (`httpRateLimit`, `httpCircuitOpen`). Toda chamada leva
`X-Memberfy-Run-Id`. O histórico só guarda método, domínio, status, duração (`durationMs`, `timedOut`), `fixedIp`, tamanho e os nomes dos segredos usados;
- `ai` ("Claude" ou "ChatGPT"): `{ provider: anthropic|openai, model, keyEnv, system?, prompt, outputFormat:
text|json, jsonSchema?, maxOutputTokens (16 a 8000, padrão 1024), timeoutSeconds (5 a 60, padrão 30), next, error }`. Usa
sempre a chave da própria comunidade (`keyEnv`, um nome do cofre que pode ir para `api.anthropic.com` ou `api.openai.com`);
nunca chave nem cota de IA da Memberfy. Modelos em `GET /api/workflows/triggers` → `aiProviders`. Salvar pede o aviso de
dados aceito (`dataNoticeRequired`) e a chave no cofre (`envUnknown`, `secretHostNotAllowed`); `{{env.*}}` não entra no
prompt. As variáveis entram como dados (marcadas), com o idioma da pessoa e o da comunidade. Saída: `steps.<nó>.output`
(texto; em JSON, o objeto e os campos em `output.<caminho>`), `tokens` e `model`; a resposta é valor de fora, como a do
HTTP: nunca no assunto do e-mail (`externalInSubject`). JSON inválido, chave recusada, recusa do modelo ou passou do
máximo: saída "erro" (`aiJsonInvalid`, `aiKeyInvalid`, `aiRefused`, `aiTruncated`); tempo esgotado, 429 ou 5xx: novas
tentativas em 1, 5 e 15 min, depois `aiUnavailable`. Tetos da comunidade em `/api/vault/ai-limits` (por execução:
`aiRunLimit`, erro; por dia: `aiDailyLimit`, espera o dia seguinte). O histórico guarda provedor, modelo, tokens e
duração, nunca a chave, o prompt nem a resposta. Na simulação, o `prompt` preenchido, sem chamar o modelo.
- `while` ("Enquanto"): `{ match, rules, body, maxLoops (1 a 10, padrão 3), next }`: repete o trecho que começa em `body` enquanto a
condição vale; o fim do trecho volta ao `while`. Todo caminho do trecho precisa de uma espera de pelo menos 1 dia; sem `while`
dentro de `while`, e nada de fora entra no trecho. No teto, sai com o motivo `loopLimit`.
- `log` ("Registrar log", fase 5): `{ level: info|warning|error, message, next }`: texto puro até 1000 caracteres depois de
preencher `{{nome}}` e `{{comunidade}}`; sai sempre por `next`. Fica no histórico da execução (passo `type: log`) e em
`GET /api/logs?type=workflows`. No máximo 20 por execução; depois, uma linha `logLimitReached`.
- `platformAction` ("Ação da plataforma"): `{ action, input, next, error }`. `action` é uma das ações de
`GET /api/workflows/triggers` → `platformActions` (cada uma com `inputSchema`, o formulário em JSON Schema, `minRole`,
`scope`, `identity` e `cost`): `member.role.set` (nunca owner nem o papel de um owner; um admin age como o owner e faz ou muda admin; nunca o próprio papel),
`member.group.remove`, `member.field.set`, `member.points.adjust` (`points` diferente de 0 e `reason`; o log de pontos
guarda o workflow, o nó e a execução, o recálculo mantém; com a gamificação desligada pula com `gamificationOff`),
`notification.send` (aviso no sino, `title` e `body`), `event.register` e `course.enroll` (só gratuitos),
`event.courtesyTicket` e `space.courtesy.grant` (`productId` com `until` ou `days`) / `space.courtesy.end` (só
cortesia; assinatura paga nunca), `content.post.create` (`spaceId` de feed ou conteúdo, `title`, `text` em Markdown; publicado em
nome de quem é o ator, com a regra "quem cria" do espaço, sem aviso por e-mail), `certificate.issue` (`templateId` de um
modelo ativo, `sendEmail`; quem já tem pula com `alreadyHas`), `recognition.give` (`message`, `spaceId` e `valueId`
opcionais; o ator reconhece a pessoa), `chat.block` (o ator bloqueia a pessoa no chat, em silêncio),
`oneOnOne.schedule` (1:1 recorrente com o líder direto: `weekday` 1-7, `time` HH:mm, `cadence`, `durationMinutes`,
`startsInDays`; sem líder pula com `noDirectLeader`, dupla com série pula com `seriesExists`) e
`developmentPlan.action.add` (`title`, `description`, `dueInDays`; no PDI ativo da pessoa ou num rascunho novo de
`planDays` dias, com `origin.type: WORKFLOW`). Dinheiro e exclusão definitiva não existem aqui. Os campos de texto (`templated`)
aceitam variáveis. `input` fora do esquema dá 422 `actionInputInvalid`; ação fora da lista, `actionUnknown`.
**Quem age:** ligar o workflow cria a chave de ator dele (uma `ApiKey` sem segredo utilizável, nunca listada, com os
escopos das ações do desenho e a pessoa que ligou); pausar, parar ou apagar revoga, retomar devolve para quem retomou.
Para as ações, admin vale como owner, menos nas marcadas `ownerOnly` (hoje nenhuma).
O papel dessa pessoa é relido a cada passo: abaixo do `minRole`, o passo pula com `actionRefused` (motivo em
`data.key`); quem ligou saiu da comunidade, `actorUnavailable` (saída de erro). Os nós `tag`,
`addToGroup`, `sendDm`, `comment`, `react` e `slackPost` passam pelo mesmo registro e pela mesma checagem;
- `runWorkflow` ("Executar outro workflow"): `{ workflowId, inputs: [{ key, value }], next, error }`. O workflow
chamado é desta comunidade e começa pelo gatilho `workflow.called` (`filter: { variables: [{ key }] }`, lidas lá como
`{{trigger.<key>}}`); chave que ele não recebe dá 422 `subWorkflowInputUnknown`, chamar a si mesmo ou quem o chama (em
qualquer ponto da cadeia) dá 422 `subWorkflowCycle`. O passo inicia uma execução do chamado para a mesma pessoa
(`parentRunId`), espera ela terminar e lê `{{steps.<nó>.output.<key>}}`, o que o `endSuccess` dele devolveu. A
reentrada, a frequência e os descadastros do chamado valem: quando ele não aceita a pessoa agora, o passo pula
(`subWorkflowSkipped`) e segue. Chamado desligado: `subWorkflowUnavailable`; terminou em erro: `subWorkflowFailed`;
a 4ª chamada em cadeia: `subWorkflowDepth` (saída de erro). Saídas de fora não vão no assunto do e-mail;
- `endSuccess` ("Fim: sucesso") e `endError` ("Fim: erro"), fase 5: sem campos (o `endSuccess` aceita `outputs:
[{ key, value }]`, até 20, o que devolve a quem o chamou). **Todo caminho termina num deles**; um
`next`/`then`/`else` vazio fora de um "Enquanto" dá 422 `code: pathWithoutEnd` com o `nodeId`. Um desenho antigo sem fins
vem no `GET` com um `endSuccess` (`end_success`) nos ramos soltos e roda igual (ramo solto = sucesso).
**Saída de erro** (fase 5): cada ação (`sendEmail`, `sendDm`, `comment`, `react`, `tag`, `addToGroup`, `slackPost`) tem
`next` ("ok") e `error` (opcional). O que é erro: falha síncrona (`emailUnavailable`, `emailNotQueued`, `itemUnavailable`,
`groupUnavailable` (grupo excluído também), `dmUnavailable`, `slackUnavailable`, `smsSendFailed`, `whatsappSendFailed`, `quotaWaitExpired`). Sem
`error` ligado, a execução termina em erro (`FAILED`) com o motivo. **Impedimento permanente é pulado** e segue por "ok"
(`optedOut`, `suppressed`, `noEmailAddress`, `blocked`, `messagesOff`, `senderNotAllowed`, `slackNotInstalled`, `slackChannelGone`; antes da fase 5g, `senderNotConsented` e `senderApprovalPending`).
**Limite temporário adia, não pula** (`reputationPaused`, `sendingSuspended`, `communityDailyLimit`, `workflowFrequencyLimit`,
`dmDailyLimit`): o passo fica `DEFERRED` ("aguardando cota", com `deferredUntil`), depois envia e segue por "ok"; o texto é
montado na liberação, em lotes ao longo de uma hora. Depois de uma suspensão do envio (`sendingSuspended`), a fila sai
num ritmo: o teto diário dividido por 24 por hora (sem teto, 10% do volume dos últimos 7 dias por hora). Teto de espera:
7 dias (no limite de frequência, a janela, até 90 dias; na suspensão, 48 horas); passado o teto, sai por "erro" com
`quotaWaitExpired`. Pausar congela a espera; retomar confere a cota de novo e recomeça a contagem; parar cancela sem
enviar, inclusive a execução que já está andando (o próximo passo de ação não sai).
**Variáveis** (fase 5b): todo nó aceita `name` (até 80 caracteres), o nome que o "Inserir variável" mostra. Os textos
(DM, comentário, Slack, log e o e-mail da biblioteca, assunto e corpo) aceitam `{{grupo.campo}}` em inglês, com valor de
reserva (`{{purchase.discount|sem desconto}}`): as do gatilho (`GET /api/workflows/triggers`, `variables`), as da pessoa
(`member.first_name`, `member.name`, `member.email`, `member.phone`, `member.role`, `member.language`, `member.member_since`,
`member.last_seen_at`, `member.tags`, `member.groups`, `member.field.<chave>`), as da comunidade (`community.name`,
`community.language`) e `steps.<nó>.result|skipped|reason|error` das ações antes do nó em todo caminho (`error` só no ramo de
erro). Os valores do evento são guardados no início da execução (até 8 KB): uma espera não os muda. Cada valor é escapado
pelo destino (HTML no e-mail e no comentário, texto na DM, sem menção nem link no Slack) e nunca vira link; `trigger.*` só
no corpo do e-mail. Variável que não chega ao nó dá 422 com item `code: variableUnavailable`, `nodeId` e o nome. Os nomes
antigos (`{{nome}}`, `{{comunidade}}`, `{{campo_x}}`, `{{gatilho_x}}`) continuam valendo.
**Regras novas** (fase 5b, critério 31): `variable {variable, op, value?, value2?, values?, days?}` compara uma variável pelo
tipo dela (texto: `eq`, `neq`, `contains`, `notContains`, `startsWith`, `endsWith`, `empty`, `notEmpty`, sem diferenciar
maiúsculas nem acentos; número: `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `empty`, dinheiro em reais, não centavos; data:
`before`, `after`, `between` com `value`/`value2` em ISO, `olderThanDays`, `newerThanDays` e `inNextDays` com `days`;
lista: `contains`, `notContains`, `containsAny`, `containsAll` com `values`; sim/não: `isTrue`, `isFalse`). `hasTag` e
`inGroup` aceitam `op` (`contains`, `notContains`, `containsAny`, `containsAll`) e `inGroup` aceita `groupIds` (até 10);
`fieldEquals` aceita `op` com os operadores do tipo do campo (sem `op`, igualdade como antes). Campo de múltipla
escolha (`MULTI_SELECT`) é lista: `containsAny` (contém algum de), `containsAll` (contém todos) e `notContains`
(não contém) com `values` (o `value` ou o rótulo das opções); sem `op`, `value` pergunta se a opção está marcada.
Na variável `{{member.field.<key>}}` ele sai como a lista de rótulos, na ordem das opções.
|