Skip to content

Edita o workflow

PATCH/api/workflows/{id}

Auth
Requires a token
Who can call it:
owner
admin
API key:
No API key accepted
X-CommunityId
X-CommunityId Send the X-CommunityId of the community the operation happens in.
Endpoint descriptions come straight from the API specification and are in Portuguese for now. The interface around them is translated.

(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.

Parameters

NameInTypeDescription
idrequiredpathstring (uuid)

Headers

NameTypeDescription
If-Unmodified-SinceoptionalstringOpcional: 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-CommunityIdoptionalstring (uuid)ID da comunidade em que a operação acontece. Obrigatório na maioria dos endpoints com escopo de comunidade.

Request body application/json

NameTypeDescription
updatedAtoptionalstring (date-time)Opcional: a versão lida (`updatedAt` do `GET`). Diferente da atual responde 409; sem ela grava sem conferir.
nameoptionalstring
descriptionoptionalstring
triggerTypeoptionalstring
triggerFilteroptionalobject
reentryoptionalONCE | AFTER_DAYS | ALWAYS
reentryDaysoptionalinteger
frequencyoptionalobject
graphoptionalobjectO 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.startrequiredstring
graph.nodesrequiredobject[]
layoutoptionalobjectPosições do editor visual `{ [nodeId]: { x, y } }` (ver `Workflow.layout`).

Responses

StatusDescription
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.

curl example

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 '{}'