Pular para o conteúdo

Edita o workflow

PATCH/api/workflows/{id}

Auth
Exige token
Quem pode:
owner
admin
Chave de API:
Não aceita chave de API
X-CommunityId
X-CommunityId Envie o X-CommunityId da comunidade em que a operação acontece.

(critério 8). Só owner e admin. Em rascunho, o desenho é editado no lugar; ligado ou pausado, vira uma versão nova para quem entrar depois, e quem está no meio termina na versão em que começou. Mudar só o `layout`, ou mandar o mesmo `graph` de novo, não cria versão.

Parâmetros

NomeOndeTipoDescrição
idobrigatóriopathstring (uuid)

Headers

NomeTipoDescrição
If-Unmodified-SinceopcionalstringOpcional: a versão lida (`updatedAt` do `GET`), como data HTTP, se não vier `updatedAt` no corpo. Outra gravação depois dela responde 409.
X-CommunityIdopcionalstring (uuid)ID da comunidade em que a operação acontece. Obrigatório na maioria dos endpoints com escopo de comunidade.

Corpo da requisição application/json

NomeTipoDescrição
updatedAtopcionalstring (date-time)Opcional: a versão lida (`updatedAt` do `GET`). Diferente da atual responde 409; sem ela grava sem conferir.
nameopcionalstring
descriptionopcionalstring
triggerTypeopcionalstring
triggerFilteropcionalobject
reentryopcionalONCE | AFTER_DAYS | ALWAYS
reentryDaysopcionalinteger
frequencyopcionalobject
graphopcionalobjectO 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.
graph.startobrigatóriostring
graph.nodesobrigatórioobject[]
layoutopcionalobjectPosições do editor visual `{ [nodeId]: { x, y } }` (ver `Workflow.layout`).

Respostas

CódigoDescrição
200O workflow.
401Token ausente, inválido ou expirado; token de outra comunidade (o `X-CommunityId` ou o `communityId` do corpo não é o do token); token antigo ou de uma sessão encerrada (troca de senha, convite aceito). Nesses casos a mensagem pede para entrar de novo (`auth.signInAgain`).
403Autenticado, mas sem permissão para esta operação.
404Recurso não encontrado.
409Outra gravação veio antes da versão enviada (`updatedAt` no corpo ou `If-Unmodified-Since`); `data.updatedAt` é a versão atual: recarregue e repita. Cada gravação avisa os editores abertos (`item.saved`, ver `POST /api/realtime/ticket`).
413Desenho maior que 64 KB (`workflows.graphTooLarge`).
422Campo ou desenho inválido; `triggerType`/`triggerFilter` num workflow cujo rascunho tem nós de gatilho (`workflows.triggerInGraph`).
500Erro interno do servidor.

Exemplo com curl

curl -X PATCH "https://api.memberfy.net/api/workflows/<id>" \
  -H "Authorization: Bearer $TOKEN" \
  -H "X-CommunityId: $COMMUNITY_ID" \
  -H "Content-Type: application/json" \
  -d '{}'