Catálogo de Statements

Cada elemento do array statements é um único Statement. Este documento cataloga os campos, o comportamento e o resultado dos 25 tipos de Statement. Toda posição de valor segue as regras de Expressões de valor (referência, literal, JsonLogic, mapa de locale). As exceções são três: o pattern de Regex, a key de Cache e a color de PdfWatermark (veja Regex, Cache e Transformação de arquivos).

Resumo dos Statements

CategoriatypeResumo em uma linha
Escritas de recursoResourceCreateCria Content/Media (opcionalmente publica)
ResourceUpdateSubstituição total dos campos de Content/Media (todo campo/locale não fornecido é excluído)
ResourcePatchMesclagem parcial dos campos de Content/Media (apenas os campos/locales especificados; um null literal exclui)
ResourceDeleteExclui (apenas Draft/Archived; se Published, anule a publicação primeiro)
ResourcePublish / ResourceUnpublishPublica / anula a publicação
ResourceArchive / ResourceUnarchiveArquiva / desarquiva
Leituras de recursoResourceReadLê um único item por id
ResourceFindPrimeiro item único correspondente por filtro (null se não houver)
ResourceForEachPercorre internamente os recursos que atendem ao filtro e executa onEach em cada item
ResourceCountConta apenas a quantidade de itens que atendem ao filtro (não lê os itens)
ExternoHttpChamada HTTP externa ({ status, body }). Pode enviar um arquivo como corpo
EmailSendEnvia 1 e-mail por meio de um EmailAccount registrado
VariáveisSetVarDeclara/atualiza uma variável com escopo de script
Armazenamento em cacheCacheLê/grava/remove no cache de vida curta exclusivo daquele Script
Parse de valoresParseJsonFaz o parse de texto JSON em um valor (objeto, array, escalar) e o vincula
Assinatura e textoSignatureVerifica se o código de assinatura recebido é igual ao código gerado com a chave secreta (Boolean)
HashCalcula um digest sem chave (string)
RegexAplica uma expressão regular. Se houve correspondência (Boolean) ou os grupos de captura (array)
Controle de fluxoIfRamificação condicional
LoopItera (foreach / while / counted)
ParallelExecuta branches de forma concorrente
ReturnRetorna um resultado e encerra antecipadamente
TryTratamento de exceções (catch/finally)

Todo Statement de Content que não indica o alvo por id precisa declarar o Content Type com que trabalha. Em ResourceFind, ResourceForEach, ResourceCount, quando resource é "Content", o contentType é obrigatório. Não existe consulta de Content que atravesse o Space inteiro. O ResourceCreate também declara o Content Type que vai criar. Media não delimita escopo, porque o Space inteiro é um único conjunto, e os Statements que indicam o alvo por id (ResourceRead, ResourceUpdate, ResourcePatch, ResourceDelete e os Statements de publicação e arquivamento) têm target, portanto não precisam de escopo.

Chamadas cíclicas são limitadas a 3. Se você ativar propagateEvents nos Statements de escrita de recursos acima (ResourceCreate, ResourceUpdate, ResourcePublish etc.), cujo padrão é vir desativado, eles geram eventos de alteração, e esses eventos podem executar um Script novamente por meio de um Webhook. Uma cadeia assim (Script → evento → Webhook → Script → …) continua no máximo 3 vezes. A partir daí, ela é interrompida automaticamente para evitar loops infinitos.

Campos comuns

{ "type": "<StatementType>", "name": "<opcional, único dentro do script>", /* ...campos específicos do tipo... */ }
  • type: o discriminador. Um dos valores da tabela acima (obrigatório).
  • name: opcional. Se definido, o resultado é vinculado ao contexto em /<name>, de modo que Statements posteriores podem referenciá-lo como { /<name>/... }. Omita-o se você não usar o resultado.
  • Regras de nome de vínculo: name é uma chave adicionada diretamente à raiz do contexto, por isso é validada ao salvar. Ela só pode usar letras ASCII (a-z, A-Z), dígitos, _ e - (precisa poder servir como chave de JSON Pointer, portanto qualquer outro caractere, ou um nome vazio, é rejeitado), não pode coincidir com as raízes reservadas (payload, rawPayload, headers, vars, error, now, secrets) e deve ser única dentro de um Script. Em caso de violação de formato, uso de palavra reservada ou duplicidade, o salvamento é rejeitado.

Formato da referência de entidade

Referências de entidade como contentType e target são unificadas em um único formato: { "sys": { "id": <expressão de valor> } }. Apenas sys.id é necessário; o tipo de destino é inferido a partir de resource (sys.type e sys.targetType são omitidos).

  • contentType.sys.id geralmente é um literal (ex.: "ct_post").
  • target.sys.id geralmente é uma expressão de valor { /ptr } (resolvida em tempo de execução; ex.: { /payload/sys/id }).

resource

Os Statements da família de recursos especificam o tipo de destino com resource: "Content" | "ContentType" | "Media" | "ServiceUser".

Content Type é aceito somente pelo ResourceCount. Se você o escrever em outro Statement, o salvamento é rejeitado. Criar ou alterar o próprio molde é da alçada da CMA, e não do Script.

Os Statements que aceitam ServiceUser (o membro inscrito no produto) são os três de leitura (ResourceRead, ResourceFind, ResourceForEach) e ResourceUpdate, ResourcePatch. Os únicos campos que esses dois podem escrever são roleOverride e enableLogin. Se você o escrever em qualquer outro Statement de escrita, o salvamento é rejeitado (consulte Erros). As regras são abordadas em Trabalhar com o diretório de membros.

Escritas de recurso

Todo Statement de escrita tem propagateEvents (padrão false) (o ResourceUpdate e o ResourcePatch que escrevem um ServiceUser não aceitam esse campo). Defini-lo como true faz com que essa escrita gere um evento de alteração, de modo que ações subsequentes como Webhook são executadas. O padrão é não gerar (uma escrita silenciosa do sistema).

ResourceCreate

Cria um Content ou Media. Content e Media compartilham o modelo fields, e os valores são mapas de locale.

CampoAplica-se aDescrição
resourceComum"Content" ou "Media" (obrigatório)
contentTypeContentO Content Type a criar ({ sys: { id } }). Obrigatório para Content
fieldsComumMapa de campos { "<field>": { "<locale>": valor } }. Cada campo preenchido exige o bucket do locale padrão. As chaves de Content seguem a definição do Content Type; as chaves de Media são fixas (title, description, file)
localeComum(Conveniência) Se fornecido, cada valor em fields é automaticamente encapsulado como { <locale>: valor }
publishComumPublica após a escrita (exposto na CDA/ACDA). Padrão true
  • Media file: o valor de fields.file.{locale} é uma instrução de ingestão { "source": <expressão de valor>, "encoding": "url"|"base64", "transformations": [ … ] }. source e encoding são obrigatórios, e transformations só é escrito quando você quer alterar o arquivo recebido antes de salvá-lo (veja Transformação de arquivos). Em uma escrita que inclui um arquivo, o motor realiza a ingestão (se for url, baixa; se for base64, decodifica, e então faz o upload e o processa). Essa ingestão não tem tempo declarado, portanto sai do orçamento base de 30 segundos (veja Orçamento de tempo) e não é contada no limite de chamadas externas. Você também pode criar um Media sem arquivo (fileless). Se publish:true mas não houver arquivo ou o processamento estiver incompleto, a etapa de publicação gera erro; se publish:false, permanece Draft.
  • Resultado (vínculo de name): o recurso criado. { /<name>/sys/id }, { /<name>/fields/<field>/<locale> }.
// Content
{ "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_post" } },
  "fields": { "title": { "en-US": "{ /payload/fields/title }" } }, "publish": true, "name": "post" }
 
// Media. file é uma instrução de ingestão
{ "type": "ResourceCreate", "resource": "Media",
  "fields": {
    "title": { "en-US": "{ /payload/fields/prompt }" },
    "file":  { "en-US": { "source": "{ /gen/body/data/0/url }", "encoding": "url" } }
  }, "name": "img" }

Transformação de arquivos (transformations)

Ao vender um e-book, você pode querer entregar uma cópia com o e-mail de quem comprou estampado em todas as páginas do PDF. Ao escrever transformations na instrução de ingestão, você pode alterar o arquivo recebido antes de salvá-lo. A única transformação suportada é PdfWatermark.

Campo da instrução de ingestãoObrigatórioDescrição
transformationsOpcionalUm array de objetos de transformação. Se for omitido ou estiver vazio, o arquivo recebido é colocado no Media sem alterações. Se não for um array, o salvamento do Script é rejeitado
  • As transformações são aplicadas na ordem do array. O resultado de uma transformação é a entrada da seguinte, e o último resultado se torna o arquivo do Media.
  • Pode ser escrito em qualquer instrução de ingestão de ResourceCreate, ResourceUpdate, ResourcePatch.
  • O original apontado por source não é alterado. Para estampar um texto em um arquivo que já está no Space, leia esse Media com ResourceRead e coloque { /<name>/fields/file/<locale>/url } em source para criar um novo Media.
  • A transformação também faz parte da ingestão. O tempo que ela leva sai do orçamento base de 30 segundos (veja Orçamento de tempo).

PdfWatermark: estampar um texto em todas as páginas do PDF

CampoObrigatórioDescrição
typeObrigatório"PdfWatermark". Com qualquer outro valor, o salvamento do Script é rejeitado
textObrigatórioO texto a estampar. É uma string e pode misturar referências { /pointer }. Não aceita objetos de operação JsonLogic. Precisa ser uma única linha, de 1 a 64 caracteres, composta de caracteres permitidos
colorOpcionalA cor do texto. É escrita como #RRGGBB (seis dígitos hexadecimais) e, se omitida, é #808080 (cinza). Aceita apenas um literal, e não uma expressão de valor; se o formato for diferente, o salvamento do Script é rejeitado
  • Os campos que não estão na tabela são ignorados. Não é possível especificar o tamanho da fonte, a posição nem a opacidade.
  • O texto é estampado em todas as páginas. Uma linha de texto é colocada ao longo da diagonal que passa pelo centro da página (do canto inferior esquerdo ao canto superior direito).
  • O texto é sobreposto ao conteúdo com 30% de opacidade. O conteúdo original sob o texto continua legível.
  • O tamanho da fonte é definido automaticamente em cada página. O texto é estampado em um tamanho que ocupa 80% do comprimento da diagonal, sem ultrapassar 15% do lado mais curto da página. Textos curtos saem grandes, textos longos saem pequenos, e o texto nunca ultrapassa os limites da página.
  • A fonte é a Helvetica, uma fonte padrão do PDF.
  • Se o arquivo é um PDF ou não é determinado pelo conteúdo, e não pelo nome ou pela extensão do arquivo.
// parte do array statements. Um novo Media com o e-mail do comprador e o número do pedido estampados no PDF do e-book comprado
{ "type": "ResourceRead", "resource": "Media",
  "target": { "sys": { "id": "{ /payload/fields/ebookMediaId }" } }, "name": "ebook" },
{ "type": "ResourceCreate", "resource": "Media", "locale": "en-US", "publish": false,
  "fields": {
    "title": "{ /ebook/fields/title/en-US }",
    "file": {
      "source": "{ /ebook/fields/file/en-US/url }", "encoding": "url",
      "transformations": [
        { "type": "PdfWatermark", "text": "{ /payload/fields/email } · { /payload/fields/orderId }", "color": "#B22222" }
      ]
    }
  }, "name": "stamped" }

Como é uma cópia para um único comprador, publish foi definido como false, para que ela não seja entregue pela CDA/ACDA. Se for omitido, a cópia é publicada conforme o padrão true.

Limites do texto do PdfWatermark

Em text, só podem ser usados os caracteres que a fonte padrão do PDF consegue desenhar (o conjunto de caracteres WinAnsi).

TipoCaracteres
Caracteres ASCII imprimíveisEspaço, letras ASCII maiúsculas e minúsculas, dígitos e símbolos básicos como @ . # { }
Caracteres Latin-1Letras e símbolos da Europa Ocidental, como é ü ñ ß · © °. Exceto o espaço não separável (U+00A0) e o hífen opcional (U+00AD)
Símbolos adicionais€ ‚ ƒ „ … † ‡ ˆ ‰ Š ‹ Œ Ž ‘ ’ “ ” • ˜ ™ š › œ ž Ÿ, além dos dois tipos de traço (U+2013, U+2014)

Nenhum outro caractere pode ser usado. Isso inclui os caracteres latinos estendidos que não estão na tabela (como ł, ő, ș), os caracteres não latinos, os emojis e também a quebra de linha e a tabulação.

Ao salvar o Script, o text é verificado tal como foi escrito. Em um text com { /pointer }, o valor resolvido só é conhecido no momento da execução, por isso esse valor é verificado novamente durante a execução. Os códigos dos erros que ocorrem ao salvar estão em Erros.

Limitetext literaltext com { /pointer }
Uma única linha de caracteres permitidosSalvamento do Script rejeitadoA string escrita inteira (incluindo os caminhos dos ponteiros) é verificada ao salvar, e o salvamento do Script é rejeitado. O valor resolvido é verificado novamente na execução, que falha com status 400
1 a 64 caracteresSalvamento do Script rejeitadoNão é verificado ao salvar. Na execução, é verificado pelo comprimento do valor resolvido, e a execução falha com status 400
  • Calcule o comprimento do texto fixo. Em "Purchased by { /payload/fields/email } · Order { /payload/fields/orderId }", o texto fixo fora do template tem 22 caracteres, portanto sobram 42 caracteres, somados, para o e-mail e o número do pedido. Se chegar um valor real mais longo que isso, o Script é salvo, mas aquela execução falha.
  • Estampe identificadores, e não nomes. Um nome de comprador que contenha caracteres fora da tabela faz a execução falhar. Um e-mail ou um número de pedido em ASCII pode ser estampado.

Quando a transformação de arquivos falha

Nos casos abaixo, esse Statement falha. Como qualquer outra falha em tempo de execução, pode ser tratada com Try/catch. A transformação ocorre antes da escrita do Media, portanto, em caso de falha, nenhum Media novo é criado e o destino de ResourceUpdate, ResourcePatch também não é alterado.

SituaçãoResultado
O arquivo de source não é um PDFFalha com status 422
Não é possível ler o PDF, ou ele está criptografado (incluindo PDFs que não pedem senha para abrir, mas apenas restringem permissões)Falha com status 422
O text resolvido está vazio ou contém apenas espaços em brancoFalha com status 400
O text resolvido está fora da faixa de 1 a 64 caracteres ou contém caracteres não permitidosFalha com status 400

ResourceUpdate

Substitui totalmente os campos do Content, Media ou ServiceUser de destino (PUT). O que você passa em fields se torna o novo conjunto de campos, e todo campo e locale que não estiver presente aqui é removido. Para alterar apenas parte dele, use ResourcePatch.

CampoDescrição
resource"Content", "Media", "ServiceUser". Os campos que podem ser escritos em um ServiceUser e as restrições estão em Trabalhar com o diretório de membros
targetO destino ({ sys: { id } }, obrigatório). O id geralmente é { /ptr }
fieldsO conjunto completo de campos a escrever. Os valores são mapas de locale. Por ser uma substituição total, todo campo e locale que não estiver presente aqui é removido. O file de Media é uma instrução de ingestão (veja ResourceCreate acima). Os arquivos listados são sempre reingeridos, e os arquivos de locales não fornecidos são excluídos
locale(Conveniência) Encapsula fields automaticamente
version(Opcional) Uma expressão de valor (Int). Bloqueio otimista. Se fornecido, a atualização é executada somente se corresponder ao sys.version atual do destino; em caso de divergência, ela é abortada com um erro de conflito de versão (que pode ser capturado com Try). Se omitido, não há verificação (last-write-wins). ServiceUser não o aceita
publishRepublica após a atualização. Padrão true

Se você usar o Update apenas para alterar os metadados de um Media, o file fica de fora e todos os arquivos são excluídos (por ser uma substituição total). Para alterações parciais, use obrigatoriamente ResourcePatch.

{ "type": "ResourceUpdate", "resource": "Content", "target": { "sys": { "id": "{ /payload/sys/id }" } },
  "fields": { "title": { "en-US": "Hello", "ko-KR": "안녕" }, "status": { "en-US": "published" } } }

ResourcePatch

Mescla parcialmente os campos do Content, Media ou ServiceUser de destino (PATCH). Ela sobrescreve apenas os campos (e os locales dentro deles) que você passa em fields e mantém intactos os campos e locales que você não menciona. O formato dos valores, locale, version e publish são os mesmos de ResourceUpdate.

CampoDescrição
resource"Content", "Media", "ServiceUser". Os campos que podem ser escritos em um ServiceUser e as restrições estão em Trabalhar com o diretório de membros
targetO destino ({ sys: { id } }, obrigatório). O id geralmente é { /ptr }
fieldsOs campos a sobrescrever. Os valores são mapas de locale. Atualiza apenas os campos e buckets de locale especificados (o restante é mantido). Se um valor for um null literal, esse par (campo, locale) é excluído. O file de Media é uma instrução de ingestão (veja ResourceCreate acima)
locale(Conveniência) Encapsula fields automaticamente
version(Opcional) O mesmo que ResourceUpdate (bloqueio otimista). ServiceUser não o aceita
publishRepublica após a atualização. Padrão true
  • Excluir um locale ou arquivo específico: forneça um null literal como valor. Por exemplo: "title": { "fr-FR": null } (exclui o título fr-FR), "file": { "en-US": null } (exclui o arquivo en-US). Uma expressão de valor que é avaliada como null em tempo de execução não é uma exclusão, mas um erro (apenas um null literal exclui).
  • Se você fornecer uma instrução de ingestão ao file de Media, ela substitui o arquivo daquele locale. Se você não fornecer um arquivo, ele é mantido.
// +1 apenas em viewCount(en-US). title, outros locales e todo o resto são mantidos como estão
{ "type": "ResourcePatch", "resource": "Content", "target": { "sys": { "id": "{ /payload/sys/id }" } },
  "fields": { "viewCount": { "en-US": { "$+": [ "{ /payload/fields/viewCount }", 1 ] } } } }

ResourceDelete

Exclui o destino. Apenas os status Draft e Archived podem ser excluídos. Se estiver Published ou Changed, a exclusão é rejeitada, portanto você deve fazer ResourceUnpublish primeiro. Para o Media, a exclusão é rejeitada enquanto o arquivo está sendo processado. Não faz auto-unpublish (igual à CMA/ACMA).

CampoDescrição
resource"Content" ou "Media"
targetO destino ({ sys: { id } }, obrigatório)
{ "type": "ResourceDelete", "resource": "Content", "target": { "sys": { "id": "{ /payload/sys/id }" } } }

ResourcePublish, ResourceUnpublish, ResourceArchive, ResourceUnarchive

Controlam de forma independente o estado de publicação e de arquivamento do destino. Os quatro compartilham os mesmos campos. A pré-condição de status de cada operação é a mesma da CMA/ACMA. O ResourcePublish não pode ser feito a partir de Archived e exige que o processamento do arquivo esteja concluído. O ResourceUnpublish só pode ser feito a partir de Published, Changed, o ResourceArchive só a partir de Draft, e o ResourceUnarchive só a partir de Archived.

CampoDescrição
resource"Content" ou "Media"
targetO destino ({ sys: { id } }, obrigatório)
version(Opcional) Uma expressão de valor (Int). Bloqueio otimista. Se fornecido, a operação é executada apenas se corresponder ao sys.version atual
{ "type": "ResourcePublish",   "resource": "Content", "target": { "sys": { "id": "{ /payload/sys/id }" } } }
{ "type": "ResourceUnpublish", "resource": "Content", "target": { "sys": { "id": "{ /payload/sys/id }" } } }
{ "type": "ResourceArchive",   "resource": "Media",   "target": { "sys": { "id": "{ /m/sys/id }" } } }

Leituras de recurso

ResourceRead e ResourceFind leem o recurso e o vinculam como valor, e ResourceCount apenas conta. Nenhum dos três altera o estado (sem propagateEvents). O ResourceForEach também é uma leitura na própria consulta, mas, se você colocar em onEach um Statement de escrita de recurso, essa escrita é executada em cada item e altera o estado.

Os quatro Statements (ResourceRead, ResourceFind, ResourceForEach, ResourceCount) usam from (padrão Current) para definir de qual versão armazenada ler. Current é o rascunho mais recente visto no estúdio de conteúdo (o valor que a CMA/ACMA lê); Published é o snapshot de publicação (o valor no momento da última publicação, entregue pela CDA/ACDA). (ServiceUser não é publicado, portanto aceita apenas Current; veja Trabalhar com o diretório de membros.)

Além disso, ResourceFind, ResourceForEach, ResourceCount ativam e desativam a Busca avançada (Advanced Search) por meio de advanced (padrão true). Se você não a declarar, ela vem ativada. É exclusiva de Content, portanto é ignorada nas leituras de Media e de ServiceUser. Quando ativada, em where é possível usar os operadores regex, near e within e a busca de texto completo (em um campo LongText com a busca de texto completo ativada, eq também encontra os itens que contêm o valor, por correspondência parcial ou aproximada), e order pode ordenar por fields.*. Quando desativada, esses três operadores são rejeitados, eq em texto é correspondência exata, e prefix e os operadores de comparação e de lista funcionam independentemente da busca avançada. Um item recém-criado ou editado leva um breve instante (cerca de 1 segundo) para ser refletido na busca avançada, portanto pode não ser capturado pela consulta de busca avançada imediatamente seguinte. Como ela vem ativada por padrão, esse atraso vale para toda consulta, a menos que você deixe advanced como false. Para ler de imediato um item recém-escrito, use ResourceRead por id (o armazenamento primário, sem esse atraso) ou consulte pelo sys.id que a escrita retornou.

O createdBy: ":self" de where significa "apenas o que o usuário que está chamando agora criou". Porém, ele não pode ser usado em um Script que permite chamada anônima (anonymousCallEnabled). Nesse caso, :self é resolvido para o autor, e não para o chamador, de modo que os recursos do autor seriam silenciosamente abertos; por isso o salvamento de uma definição assim é rejeitado (veja Chamada anônima).

Em where e order, os campos de conteúdo são escritos como fields.<field> (o nome sozinho não é reconhecido). Para fields.<field>, aplica-se automaticamente o locale padrão do Space, portanto você não anexa um locale diretamente. Os fields.status e fields.slug nos exemplos abaixo já são consultas no locale padrão. Somente para consultar apenas um locale específico (não padrão), você o indica explicitamente como fields.<field>.<locale> (por exemplo, fields.title.ko-KR). sys.* (como sys.createdAt) e createdBy (:self) são escritos tal como estão, sem fields.. As regras detalhadas estão em Expressões de valor: o locale em where e order.

Trabalhar com o diretório de membros (ServiceUser)

ResourceRead, ResourceFind, ResourceForEach aceitam "ServiceUser" em resource para ler o diretório de membros daquele Space (o ResourceCount não o aceita; veja ResourceCount abaixo). Use-os em fluxos como conferir quem é o dono de um pedido, ou encontrar um membro pelo e-mail e passar o sys.id dele ao Statement seguinte. As regras abaixo valem para os cinco Statements que trabalham com o diretório de membros.

  • São apenas dois os campos que podem ser alterados. ResourceUpdate e ResourcePatch aceitam "ServiceUser" e alteram roleOverride e enableLogin. No ResourcePatch, passar null em roleOverride remove o papel que estava definido. ResourceCreate, ResourceDelete e os Statements de publicação e arquivamento não aceitam "ServiceUser", e uma definição assim é rejeitada no momento do salvamento. Não é algo que se libere acrescentando permissões: o Script não pode criar nem excluir um membro, portanto a recusa não vem de um erro de permissão, mas de um Statement escrito de forma inválida.
  • O membro não tem version nem propagateEvents. Escrever qualquer um dos dois faz a execução falhar. Como o ResourceUpdate substitui todos os campos que podem ser escritos, o enableLogin é obrigatório nele, e não informar roleOverride remove o papel que estava definido. Para alterar apenas um dos dois, use o ResourcePatch; nesse caso, é preciso informar ao menos um dos dois campos.
  • O autor precisa ter permissão sobre o diretório de membros para que a definição seja salva. A verificação não usa o mapa de permissões, como em Content e Media, mas confere se o settings do SpaceRole do autor tem SETTING_SERVICE_LOGIN (ou SETTING_ALL). Isso porque o diretório de membros é, em todos os outros caminhos também, um recurso governado pelas configurações do Space. Se não tiver, o salvamento é rejeitado (veja Modelo de segurança).
  • from aceita apenas Current. Como o membro não é um recurso publicado, passar Published faz a execução falhar.
  • contentType e advanced são ignorados. O diretório de membros não se divide por Content Type (é um único conjunto para todo o Space), e a busca avançada também é exclusiva de Content.
  • O sys.email de where aceita apenas operadores da família de igualdade exata (eq, ne, in, nin). Como o endereço do membro é armazenado criptografado, comparações de ordem e prefix não têm sentido. Se você passar outro operador, em vez de devolver silenciosamente 0 resultado, a execução falha.
  • O resultado é o próprio recurso ServiceUser. Referencie-o como { /<name>/sys/id } ou { /<name>/nickname }. A estrutura é abordada na referência de ServiceUser. Para enviar um e-mail ao membro encontrado, não extraia o endereço: passe o sys.id dele ao toServiceUser de EmailSend (o motor resolve o endereço imediatamente antes do envio, de modo que o endereço do membro não entra no espaço de variáveis do Script).
// encontra um membro pelo e-mail. null se não houver
{ "type": "ResourceFind", "resource": "ServiceUser",
  "where": { "sys.email": { "eq": "{ /payload/fields/email }" } }, "name": "member" }

ResourceRead

Lê um único item por id (get-by-id). O resultado vincula o recurso inteiro ao nome.

CampoDescrição
resource"Content", "Media", "ServiceUser"
targetO destino ({ sys: { id } }). O id é uma expressão de valor
from(Opcional) Current (padrão, o rascunho mais recente) ou Published (o snapshot de publicação). Para ServiceUser, apenas Current
  • Resultado: o que é vinculado é o próprio recurso. Se você deu um name a este Statement, referencie-o diretamente como { /<name>/sys/id } e { /<name>/fields/<field>/<locale> } (com o "name": "order" do exemplo abaixo, { /order/sys/id }). Não é uma lista, então nenhum índice de array entra em jogo.
  • Se o destino não existir, gera erro. Você pode envolvê-lo em Try para tratar isso.
{ "type": "ResourceRead", "resource": "Content",
  "target": { "sys": { "id": "{ /payload/fields/orderId }" } }, "name": "order" }

ResourceFind

Lê o primeiro item único correspondente por filtro. Se não houver nenhum, é null. Use-o para encontrar um registro por uma chave de negócio única (slug, email, sku).

CampoDescrição
resource"Content", "Media", "ServiceUser"
contentTypeO Content Type no qual pesquisar ({ sys: { id } }). Obrigatório quando é Content. Ignorado em Media e ServiceUser
whereO filtro ({ "<field>": { "<op>": <valor> } }). Os operadores são os da lista de operadores (regex/near/within exigem advanced). createdBy: ":self" suportado. Para o sys.email de ServiceUser, apenas eq, ne, in, nin (Trabalhar com o diretório de membros)
orderA ordenação que determina o "primeiro" quando há várias correspondências (ex.: "-sys.createdAt")
from(Opcional) Current (padrão, o rascunho mais recente) ou Published (o snapshot de publicação). Para ServiceUser, apenas Current
advanced(Opcional) Executar via busca avançada (Advanced Search). Somente Content (Media e ServiceUser são ignorados). Padrão true. Veja a nota Leituras de recurso acima
  • Resultado: vincula o primeiro recurso correspondente ao name deste Statement. Referencie-o diretamente como { /<name>/fields/<field>/<locale> }. Como é null quando não há nenhum, ramifique conforme a existência com { "==": [ "{ /<name> }", null ] } (o padrão típico find-then-upsert).
{ "type": "ResourceFind", "resource": "Content", "contentType": { "sys": { "id": "ct_article" } },
  "where": { "fields.slug": { "eq": "{ /payload/fields/slug }" } }, "name": "found" }

ResourceForEach

Percorre internamente os recursos que atendem ao filtro e executa onEach em cada item. É um Statement para realizar uma operação em cada item, sem construir uma coleção para usar como valor. Use-o em trabalhos repetitivos como publicar rascunhos em lote, editar em lote os Content que atendem a uma condição, ou enviar/sincronizar cada item para fora. Para ler um único registro, use ResourceRead (id) ou ResourceFind (filtro).

CampoDescrição
resource"Content", "Media", "ServiceUser" (obrigatório)
contentTypeO Content Type do escopo do percurso ({ sys: { id } }). Obrigatório quando é Content. Ignorado em Media e ServiceUser
whereO filtro ({ "<field>": { "<op>": <valor> } }). O significado é o mesmo do where de ResourceFind (a restrição do sys.email de ServiceUser também é idêntica). Os operadores são os da lista de operadores (regex/near/within exigem advanced). createdBy: ":self" suportado
orderA ordenação (ex.: "sys.createdAt,sys.id"). Se ausente, a ordem padrão da plataforma
fromCurrent (padrão, o rascunho mais recente) ou Published (o snapshot de publicação). Para ServiceUser, apenas Current
advancedPercorre via busca avançada (Advanced Search). Somente Content (Media e ServiceUser ignorados). Padrão true. Veja a nota Leituras de recurso acima
limit(Opcional, 1 ou mais) O limite máximo do total de itens processados (não é o tamanho da página). Se ausente, percorre até o limite máximo da plataforma (10.000 itens)
name(Opcional) O nome ao qual vincular o item atual. É revinculado a cada iteração e referenciado dentro de onEach como { /<name> } (mesma vida útil do name de Loop; permanece vinculado ao último item mesmo após o fim do percurso). Omita se você não referenciar o item
onEachO array de Statements filhos a executar em cada item (obrigatório)
  • Não vincula uma coleção (foreach, não map). Não há { items, next } nem cursor. Você não recebe o resultado do percurso como um valor; em vez disso, executa onEach em cada item. Se precisar de uma lista, colete você mesmo com SetVar. Se precisar apenas da contagem, use o ResourceCount.
  • Mesmo sem limit, não é um percurso infinito. Sem ele, percorre até o limite máximo da plataforma (10.000 itens) e, se atingir esse limite ainda havendo correspondências, falha (para não reportar sucesso deixando itens intocados). Por outro lado, atingir o limit declarado é uma parada intencional, portanto um encerramento normal. Um limit que ultrapasse o limite máximo é recusado ao salvar.
  • Não há cursor. Se concluir tudo, é sucesso; se for interrompido no meio (estouro de relógio de parede ou de cota, uma falha não tratada em onEach), é falha, e o erro aponta em qual item e por que falhou. A retomada é expressa pelo autor com os próprios dados (deixando where como "não processado" e marcando a conclusão ao fim de onEach, uma reexecução continua a partir do que restou).
  • No orçamento de tempo, ele entra como multiplicação. O tempo que este Statement declara é o tempo declarado por onEach multiplicado pela quantidade de itens processados (limit; 10.000 se não houver) (veja Orçamento de tempo). Por ser um Statement composto que possui filhos, ele próprio não conta no orçamento de chamadas externas leaf; são os Statements de chamada externa dentro de onEach que entram no orçamento.
  • Em onEach, como em qualquer outro Statement, você pode incluir chamadas externas (Http, EmailSend) ou ingestão de arquivo de Media (igual ao body de Loop). A razão de existir deste Statement é processar o resultado de uma consulta de recurso uma vez por item.
// encontra todos os posts em rascunho e publica cada item
{ "type": "ResourceForEach", "resource": "Content", "contentType": { "sys": { "id": "ct_post" } },
  "where": { "fields.status": { "eq": "draft" } }, "order": "sys.createdAt,sys.id",
  "from": "Current", "advanced": false, "name": "post",
  "onEach": [
    { "type": "ResourcePublish", "resource": "Content", "target": { "sys": { "id": "{ /post/sys/id }" } } }
  ] }

ResourceCount

Conta apenas a quantidade de itens que atendem ao filtro. Como não traz os itens, use-o quando você precisa do número, e não da lista. É o lugar para conferir o estoque restante, julgar se já existe um registro com o mesmo valor, ou verificar se um limite foi ultrapassado.

CampoDescrição
resource"Content" ou "ContentType" (obrigatório). Media e ServiceUser não podem ser contados, e escrevê-los assim faz o salvamento ser rejeitado
contentTypeO Content Type do escopo da contagem ({ sys: { id } }). Obrigatório quando é Content. Ao contar Content Type, é ignorado (o Space inteiro é um único conjunto)
whereO filtro. O significado é o mesmo do where de ResourceFind. Conta todos os itens correspondentes
from(Opcional) Current (padrão, o rascunho mais recente) ou Published (o snapshot de publicação)
advanced(Opcional) Executar via busca avançada (Advanced Search). Somente Content (ignorado ao contar Content Type). Padrão true. Veja a nota Leituras de recurso acima
name(Opcional) O nome ao qual vincular a contagem
  • Resultado: vincula a quantidade de correspondências ao name deste Statement. Referencie-a como { /<name> } para usá-la em comparações e ramificações.
  • Ele não devolve os itens. Se você precisa dos itens, use ResourceFind (o primeiro item único correspondente) ou ResourceForEach (executa em cada item).
  • Não percorra com ResourceForEach só para obter uma contagem. O percurso reserva o orçamento de tempo multiplicado pela quantidade de itens (veja Orçamento de tempo) e falha se atingir o limite máximo da plataforma ainda havendo correspondências. Se você só precisa contar, este Statement resolve de uma vez.
  • Não existem order nem limit. Para contar não é necessária uma ordem, e tudo o que corresponde é contado.
// conta quantos comentários este post tem
{ "type": "ResourceCount", "resource": "Content", "contentType": { "sys": { "id": "ct_comment" } },
  "where": { "fields.postId": { "eq": "{ /payload/sys/id }" } }, "name": "commentCount" }

Externo

Http

Chama um endpoint HTTP externo. Por ser uma chamada externa, ele é contado no limite de chamadas externas por plano. Ao orçamento de tempo soma-se o timeoutMs e, se você enviar arquivos, soma-se também o tempo de download de cada arquivo. Se você especificar retry, essa soma é multiplicada pelo número de tentativas (1 + retry).

CampoDescrição
method"GET", "POST", "PUT", "PATCH", "DELETE"
urlA URL de destino (uma expressão de valor; { /ptr } pode ser interpolado)
headers[{ "key", "value" }]. value é uma expressão de valor. Valores secretos, como chaves de API, não são escritos literalmente: declare-os em secrets e leia-os com { /secrets/<key> } (ex.: "Bearer { /secrets/LLM_API_KEY }"). Se você colocar Content-Type aqui, o body é serializado nesse formato (abaixo)
bodyO corpo da requisição (uma expressão de valor ou JSON). O formato em que ele é enviado é definido pelo cabeçalho Content-Type
urlBodyA URL do arquivo a enviar como corpo da requisição (expressão de valor). A plataforma baixa o arquivo dessa URL e envia os bytes como estão (Enviar um arquivo como corpo)
multipartBodyO array de parts a enviar como multipart/form-data (1 ou mais). É possível misturar parts de texto e parts de arquivo (Enviar um arquivo como corpo)
timeoutMsO tempo limite desta chamada (ms)
retryO número de novas tentativas quando o status da resposta é 400 ou superior. Padrão 0; limite máximo 2
ignoreStatusCodeSe esta chamada deve ser tratada como uma falha quando o status final (após as novas tentativas) é 400 ou superior. Quando false (padrão), ela é tratada como uma falha e se torna um alvo de Try/catch. Quando true, ela não é tratada como uma falha e { status, body } é vinculado como está (o chamador ramifica com base no próprio status)
responseTypeComo o corpo da resposta é recebido. "Json" (padrão) faz o parse dele em objeto ou array; "Text" recebe como string
  • Use apenas um entre body, urlBody e multipartBody. Se você escrever dois ou mais, o salvamento é rejeitado. Se não houver nenhum dos três, a requisição é enviada sem corpo.
  • Resultado: { status, body }. Se você deu um name a este Statement, { /<name>/status }, { /<name>/body/... }. A forma de body é definida por responseType.
  • responseType se aplica apenas a uma resposta bem-sucedida. O corpo de uma resposta com status 400 ou superior é vinculado para diagnóstico independentemente do valor declarado (o valor parseado quando é JSON, uma string caso contrário).
  • Se for "Json" e o corpo não for JSON, esta chamada falha (alvo de Try/catch). Para APIs que não devolvem JSON, receba o corpo como "Text" e faça o parse com ParseJson quando precisar tratá-lo como valor.
  • "Text" é decodificado com o charset do Content-Type da resposta e tratado como UTF-8 quando não há charset. Se o corpo estiver vazio, body é null nos dois casos.
  • Limite de tamanho da resposta: O corpo da resposta tem no máximo 10MiB. Se exceder isso, esta chamada falha com uma exceção e pode ser tratada como qualquer outra falha em tempo de execução com Try/catch (é uma falha baseada em tamanho, portanto não é suprimida por ignoreStatusCode).
{ "type": "Http", "method": "POST", "url": "https://api.llm.com/v1/gen",
  "headers": [ { "key": "Authorization", "value": "Bearer { /secrets/LLM_API_KEY }" } ],
  "body": { "prompt": "{ /payload/fields/prompt }" }, "timeoutMs": 15000, "retry": 1,
  "responseType": "Json", "name": "resp" }

Em que formato o body é enviado

O Content-Type que você coloca em headers define o formato de serialização do body. A comparação ignora maiúsculas e minúsculas e parâmetros como ;charset=…, olhando apenas a parte inicial. Quando o cabeçalho está ausente ou o valor está vazio, a requisição é enviada como application/json. Esse cabeçalho só é anexado quando existe um body, portanto, se não houver body, o cabeçalho que você escreveu é enviado como está. Se você colocar a mesma chave várias vezes, apenas o primeiro valor é usado e elas são unificadas em uma só.

Um body que não cabe no formato declarado é enviado com uma correção para um formato em que ele caiba. O cabeçalho nunca diz algo diferente do corpo que é realmente enviado.

Estas são as combinações que são enviadas exatamente como declaradas.

Content-Type declaradoForma do bodybody enviado
application/jsonQualquer umaJSON
application/x-www-form-urlencodedObjeto ou arrayorder[id]=A-2481&order[amount]=34000
text/plainEscalarO valor como está
Outros (text/xml etc.)Qualquer umaJSON

Estas são as combinações que são corrigidas porque não cabem no formato declarado.

Content-Type declaradoForma do bodyContent-Type realmente enviadobody enviado
application/x-www-form-urlencodedEscalartext/plain;charset=UTF-8O valor como está
text/plainObjeto ou arrayapplication/jsonJSON

Estas duas linhas esclarecem como a requisição é enviada quando o par está desalinhado, e não são uma forma de obter o formato pretendido. Quando o body é montado por uma expressão de valor, ele pode resultar em um escalar conforme o payload do momento da execução, e nesse caso a correção acontece sem erro. Se o lado que recebe questionar o formato, ajuste a forma do body ou o Content-Type de acordo com a sua intenção.

O form-urlencoded expande objetos em chaves com colchetes e arrays em índices.

bodyChaves e valores expandidos
{ "order": { "id": "A-2481", "amount": 34000 } }order[id]=A-2481&order[amount]=34000
{ "tags": ["outerwear", "winter"] }tags[0]=outerwear&tags[1]=winter
{ "items": [{ "sku": "TUMBLER-500" }] }items[0][sku]=TUMBLER-500
{ "memo": null }memo=

As chaves e os valores são enviados com codificação percentual em UTF-8. A tabela acima está na forma decodificada para mostrar a estrutura das chaves. Mesmo que um valor contenha & ou +, ele é transmitido como está, sem ser confundido com um separador de pares ou um espaço.

A notação que expande o aninhamento em chaves com colchetes é uma convenção amplamente usada, e não uma especificação do próprio formato. Verifique se o lado que recebe restaura order[id] como um objeto aninhado e, se não restaurar, monte o body com chaves planas.

{ "type": "Http", "method": "POST", "url": "https://api.example.com/oauth/token",
  "headers": [ { "key": "Content-Type", "value": "application/x-www-form-urlencoded" } ],
  "body": { "grant_type": "client_credentials", "client_id": "{ /vars/clientId }" },
  "name": "token" }

Enviar um arquivo como corpo

Às vezes é preciso repassar a uma API externa um arquivo que você enviou para um Media. Os bytes de um arquivo não cabem em uma expressão de valor. Em vez disso, se você especificar a URL onde está o arquivo, a plataforma baixa o arquivo dessa URL e o envia como corpo da requisição. Para enviar um único arquivo como o corpo inteiro, use urlBody; para enviar arquivos e campos de texto juntos como formulário, use multipartBody.

urlBody: enviar um único arquivo como o corpo inteiro

O arquivo baixado da URL especificada é enviado como corpo da requisição tal como está, sem conversão. É adequado para APIs que recebem o arquivo por PUT em uma URL de upload. Abaixo está um exemplo que lê o PDF de um contrato (Media) e o envia para a URL de upload de um serviço de assinatura eletrônica.

// parte do array statements
{ "type": "ResourceRead", "resource": "Media",
  "target": { "sys": { "id": "{ /payload/fields/contractMediaId }" } }, "name": "contract" },
{ "type": "Http", "method": "PUT",
  "url": "https://api.esign.example/v1/envelopes/{ /payload/fields/envelopeId }/document",
  "headers": [ { "key": "Authorization", "value": "Bearer { /secrets/ESIGN_API_KEY }" },
               { "key": "Content-Type", "value": "application/pdf" } ],
  "urlBody": "{ /contract/fields/file/en-US/url }", "name": "upload" }

O Content-Type da requisição é definido na ordem a seguir.

  1. Se você especificou Content-Type em headers, esse valor é usado como está. A correção da serialização do body não se aplica.
  2. Se não especificou ou o valor estiver vazio, é usado o Content-Type com que a URL do arquivo respondeu.
  3. Se ele também não existir, é usado application/octet-stream.

multipartBody: enviar arquivos e texto como formulário

A requisição é enviada como multipart/form-data. Cada elemento do array vira uma part, e as parts são enviadas na ordem do array. O array precisa ter 1 ou mais elementos; um array vazio faz o salvamento ser rejeitado. O Content-Type é sempre multipart/form-data (a string de delimitação é gerada automaticamente), e o Content-Type escrito em headers é ignorado.

Campo da partDescrição
nameO nome do campo do formulário (obrigatório). Não é uma expressão de valor, mas um literal. Se você usar o mesmo nome várias vezes, cada um é enviado como uma part separada
valueO conteúdo de uma part de texto (expressão de valor). Se o resultado for vazio, é enviado como valor vazio
urlA URL de onde baixar os bytes de uma part de arquivo (expressão de valor). Ex.: a url de um arquivo de Media
fileNameO nome do arquivo colocado no Content-Disposition da part (expressão de valor). Pode ser usado apenas em parts com url
contentTypeO Content-Type da part (expressão de valor)
  • Escreva exatamente um entre value e url. Se você escrever os dois ou nenhum, o salvamento é rejeitado. Escrever fileName em uma part com value também faz o salvamento ser rejeitado.
  • Se você omitir fileName, é usado o último segmento do caminho da url, decodificado (sem a query string). Se esse segmento estiver vazio ou contiver caracteres de controle, o nome enviado é file.
  • Se você omitir contentType, uma part com value é enviada sem Content-Type, e uma part com url usa o Content-Type com que a URL do arquivo respondeu (application/octet-stream se não houver). Se você especificar um valor que não está no formato de media type, esse Http falha com status 400 na execução.
  • Se o resultado da expressão de valor de fileName ou contentType contiver um caractere de quebra de linha, esse Http falha com status 400 antes de enviar a requisição.

Abaixo está um exemplo que envia uma foto de produto (Media) junto com opções de análise para um serviço de análise de imagens. Como a part image não tem fileName, o último segmento da URL do arquivo de Media (ex.: tumbler.png) é enviado como nome do arquivo.

// parte do array statements
{ "type": "ResourceRead", "resource": "Media",
  "target": { "sys": { "id": "{ /payload/fields/photoMediaId }" } }, "name": "photo" },
{ "type": "Http", "method": "POST", "url": "https://api.vision.example/v2/analyze",
  "headers": [ { "key": "x-api-key", "value": "{ /secrets/VISION_API_KEY }" } ],
  "multipartBody": [
    { "name": "image", "url": "{ /photo/fields/file/en-US/url }" },
    { "name": "language", "value": "ko" },
    { "name": "features", "value": "labels" },
    { "name": "features", "value": "text" }
  ], "timeoutMs": 20000, "name": "analysis" }

As falhas de um Http que envia arquivos também se dividem em dois tipos. O estouro de tempo é tratado como uma resposta com status 504, portanto retry e ignoreStatusCode se aplicam. Às falhas relacionadas a arquivos da tabela abaixo, nenhum dos dois se aplica. A classificação completa das falhas de Http está em As duas formas de falha do Http.

Com ignoreStatusCode: true, quando o tempo se esgota, { "status": 504, "body": "timed out after …ms" } é vinculado como resultado.

Uma requisição precisa terminar dentro de timeoutMs, e o tempo de espera pela resposta da URL do arquivo também está incluído nele. Ao enviar um arquivo, o tempo limite aumenta no tempo necessário para enviar esse arquivo à velocidade de 10.000.000 bytes por segundo. O tempo acrescido não ultrapassa o tempo de download do plano multiplicado pelo número de arquivos. Porém, se a URL do arquivo não informar o tamanho do arquivo, o tempo aumenta apenas na medida do que foi efetivamente enviado, portanto, se a transferência for lenta ou parar no meio, o tempo pode se esgotar mais cedo.

Nos casos abaixo, esse Http falha imediatamente. Não há nova tentativa com retry, e a chamada é tratada como falha mesmo com ignoreStatusCode: true. Como qualquer outra falha em tempo de execução, pode ser tratada com Try/catch.

SituaçãoResultado
O arquivo a baixar é maior que o limite de download do planoFalha com status 413. Se a URL do arquivo informar o tamanho de antemão, falha antes do download; se não informar, falha no momento em que o limite é ultrapassado durante o download
A URL do arquivo respondeu com um status que não é 2xx (incluindo redirecionamentos), ou não foi possível conectar-se à URL do arquivoFalha com status 502
O contentType de uma part não está no formato de media typeFalha com status 400
O valor de fileName, contentType de uma part contém um caractere de quebra de linhaFalha com status 400. Falha antes de enviar a requisição
  • O limite de download se aplica por URL de arquivo. Se multipartBody tiver várias parts de arquivo, o limite se aplica separadamente a cada arquivo.
  • Os bytes enviados, incluindo os arquivos, são somados ao item "Chamadas externas" da transferência de dados do plano.

EmailSend

Envia 1 e-mail por meio de um EmailAccount registrado. Os campos aceitos são apenas os que mapeiam diretamente para SMTP/MIME. Não há id de template, envio agendado nem extensões por provedor (se você precisar de recursos assim, chame diretamente a API do serviço de e-mail correspondente com Http). O remetente (o endereço que envia) não é definido aqui, mas vem do EmailAccount apontado por account.

CampoDescrição
accountReferência ao EmailAccount por onde enviar ({ sys: { id } }, obrigatório). Geralmente um id literal. Se fornecido como expressão de valor, é resolvido no momento do envio, portanto não pode ser verificado ao salvar
toEndereço do destinatário (expressão de valor). Use exatamente um entre to e toServiceUser
toServiceUserEspecifica o destinatário como referência a um ServiceUser ({ sys: { id } }; esse sys.id pode ser uma expressão de valor). O motor resolve o endereço imediatamente antes do envio, de modo que o endereço do membro não entra no espaço de variáveis do Script
ccArray de endereços em cópia (expressão de valor)
bccArray de endereços em cópia oculta (expressão de valor)
subjectO assunto (expressão de valor, obrigatório)
bodyO corpo (expressão de valor). Sempre enviado como text/html, portanto escreva markup, não texto puro (quebras de linha viram espaço, < é interpretado como tag). O resultado das expressões de valor interpoladas passa por escape de HTML, a menos que autoEscape seja false. Use exatamente um entre body e templateBody
templateBodyMonta o corpo a partir de um modelo. Tem o formato { source, data } e você usa exatamente um entre ele e body. Veja Montar o corpo com um modelo abaixo
autoEscape(Opcional, padrão true) Define se os valores interpolados no corpo passam por escape de HTML. Os alvos são o resultado de { /pointer } em body e os valores de templateBody.data. Se você deixar false, o markup contido nos valores é renderizado como está. O markup que o próprio autor escreveu e o modelo de templateBody.source não são alvos desde o início, e os campos de cabeçalho também não são afetados
replyTo(Opcional) O cabeçalho Reply-To (expressão de valor). Pode ser diferente do remetente (ex.: enviar como no-reply, mas com as respostas indo para um endereço de suporte)
timeoutMs(Opcional, 1 ou mais) O tempo limite deste envio (ms). Se ausente, o padrão da plataforma; um valor que ultrapasse o limite máximo é recusado ao salvar
  • O corpo é escrito com body ou com templateBody, um dos dois. Se você escrever os dois juntos ou deixar os dois vazios, o salvamento é rejeitado.
  • Por padrão, o valor enviado por quem chama é renderizado como texto. Com autoEscape ligado, um markup como <a> contido no valor aparece como texto, não como tag. É o mecanismo que impede que um valor enviado por outra pessoa plante links ou tags no e-mail de alguém. Deixe false apenas quando o markup precisa valer, como no corpo de uma newsletter que o administrador escreveu.
  • Os campos de cabeçalho não aceitam valores com quebra de linha. Isso vale para subject, replyTo e todos os endereços. Se entrar uma quebra de linha, a execução falha. No corpo, a quebra de linha é conteúdo comum e não é verificada.
  • O total de destinatários é no máximo 50. Conta-se somando to (1), cc e bcc (o envelope SMTP não distingue cc/bcc e todos saem como destinatários, por isso a contagem é somada). Se ultrapassar, é recusado ao salvar e na execução. Para enviar a muitas pessoas, use ResourceForEach + EmailSend para enviar 1 e-mail por item.
  • Não vincula resultado. O sucesso significa apenas que "o provedor aceitou o e-mail", portanto não há valor a devolver e ele não aceita name. Também não faz novas tentativas (o e-mail não é idempotente; tentar de novo após uma falha ambígua causaria envio duplicado, por isso ele não segue o retry de Http). A falha é lançada (throw) e tratada pelo catch de um Try.
  • É uma chamada externa. Ela é contada no limite de chamadas externas por plano e, no orçamento de tempo, entra como timeoutMs (10 segundos se ausente) uma única vez (como ela não tenta de novo, não se multiplica pela quantidade de tentativas, como em Http). Pode ser usada dentro de onEach de um ResourceForEach (a forma padrão de envio em lote).
{ "type": "EmailSend", "account": { "sys": { "id": "eml_orders" } },
  "to": "{ /order/fields/email/en-US }",
  "subject": "Pedido recebido com sucesso (número do pedido { /order/sys/id })",
  "body": "<p>Seu pedido foi recebido. Avisaremos novamente quando a entrega começar.</p>",
  "replyTo": "support@my-shop.example" }

Montar o corpo com um modelo (templateBody)

Escrever o corpo do e-mail diretamente na definição do Script obriga a editar o Script a cada mudança de texto. O templateBody mantém o modelo do corpo em um Content e preenche apenas as lacunas dele, de modo que o texto é editado no estúdio de conteúdo e o Script permanece como está.

ChaveDescrição
sourceUm único ponteiro para o lugar onde está o modelo (obrigatório). Ele precisa apontar para um Content que o ResourceRead ou o ResourceFind deste Script leu, e nenhum outro caractere pode ficar antes ou depois desse ponteiro. O valor tem de ser uma string, portanto um Field multilíngue leva também o locale (/tpl/fields/html/en-US)
dataOs valores que preenchem as lacunas do modelo (opcional). O nome da chave é o nome da lacuna. É uma posição de dados, portanto um operador precisa do prefixo $ ($cat), e uma lacuna cujo nome começa com $ se escreve $$ (veja Posições de dados e posições de expressão)

Uma lacuna dentro do modelo se escreve { /<chave> }. Essas lacunas leem apenas data; um modelo não pode ler /payload nem /secrets diretamente. Uma lacuna que aponta para uma chave ausente em data é renderizada como texto vazio. O mesmo vale para uma chave cujo valor é null. É o contrário de um ponteiro em body que aponta para null e imprime o texto null, portanto tenha cuidado ao alternar entre as duas formas.

Onde o modelo tiver um { seguido de / (inclusive com espaço entre eles, por exemplo o a{/*comentário*/} do CSS), isso é lido como uma lacuna, então escreva \{.

Há três lugares em que o salvamento passa, mas a execução para. Falha quando source resolve para um objeto (quando o locale foi esquecido em um Field multilíngue). Falha quando source está vazio (ou o ResourceFind não encontrou nada, ou o Statement que lê o modelo está depois do EmailSend). E falha também quando o resultado de preencher todas as lacunas é apenas espaço em branco.

O modelo para o qual source aponta não passa por escape. O que passa por escape são os valores de data. Por isso, para levar ao e-mail um HTML enviado por quem chama, coloque esse valor em data e deixe autoEscape como false.

{ "type": "ResourceRead", "resource": "Content", "name": "tpl",
  "target": { "sys": { "id": "cnt_mail_shipping" } } },
{ "type": "EmailSend", "account": { "sys": { "id": "eml_orders" } },
  "toServiceUser": { "sys": { "id": "{ /order/fields/buyer/en-US/sys/id }" } },
  "subject": "Sua entrega foi iniciada",
  "templateBody": {
    "source": "{ /tpl/fields/html/en-US }",
    "data": { "name": "{ /order/fields/buyerName/en-US }",
              "tracking": "{ /order/fields/trackingNo/en-US }" } } }

O modelo (o Field html de cnt_mail_shipping) tem este formato.

<p>{ /name }, a entrega do produto que você pediu foi iniciada.</p>
<p>Número de rastreio: { /tracking }</p>

Variáveis

SetVar

Declara ou atualiza uma variável mutável com escopo de script. Referencie-a como { /vars/<var> } (o JsonLogic não tem declaração de variável, por isso ela é fornecida como um Statement).

CampoDescrição
varO nome da variável. Referenciada como { /vars/<var> }
valueUma expressão de valor. Pode referenciar a si mesma para acumular
{ "type": "SetVar", "var": "total", "value": 0 }
{ "type": "SetVar", "var": "total", "value": { "$+": [ "{ /vars/total }", "{ /row/qty }" ] } }   // acumula
{ "type": "SetVar", "var": "ids",   "value": { "$merge": [ "{ /vars/ids }", [ "{ /row/sys/id }" ] ] } }  // coleta em array

Armazenamento em cache

Cache

Lê e grava no cache de vida curta exclusivo daquele Script. É a posição para segurar por alguns segundos um valor que não compensa buscar de novo a cada execução, como o resultado de uma chamada externa, e reutilizá-lo na chamada seguinte. Não é uma chamada externa, portanto não entra no número de chamadas externas por definição e também não declara tempo algum no orçamento de tempo.

CampoDescrição
actionUm entre "Set" (gravar), "Get" (ler) e "Delete" (remover) (obrigatório)
keyA chave de cache (Cache Key) (obrigatório). Não é uma expressão de valor, mas um literal (veja abaixo). No máximo 128 caracteres; acima disso, o salvamento é rejeitado
valueO valor a guardar (exclusivo de Set)
ttlO tempo em que o cache permanece vivo (exclusivo de Set, em segundos). Entre 1 e 30; se omitido, 5
defaultValueO valor que Get vincula quando não há dados em cache (exclusivo de Get). Se omitido, null
nameO nome que guarda o resultado. Em Get é obrigatório (se o valor lido não tem para onde ir, não há razão para lê-lo). Set vincula o valor guardado e Delete vincula se a remoção foi realizada; nos dois é opcional
  • Escreva apenas os campos que correspondem à operação. Se você escrever ttl em um Get ou defaultValue em um Set, o salvamento é rejeitado.
  • Não existir e ter expirado não se distinguem. Nos dois casos, o que é vinculado é defaultValue. O mesmo vale quando o que está guardado é null.
  • O escopo de armazenamento é aquele Script, e só ele. Outro Script do mesmo Space não enxerga os dados dele, ainda que use a mesma chave de cache. Se aquele Script for editado ou excluído, todos os dados dele desaparecem.
  • key é um literal. Deixar que os dados sejam escolhidos por uma chave de cache vinda da requisição faria o chamador decidir o que é lido, e um Script que guarda um valor por membro passaria o valor de um membro a outro. Por isso, um { /pointer } dentro de key não é convertido em valor nem é usado literalmente: o próprio salvamento é rejeitado.
  • Não pode ficar dentro de uma iteração. Se houver um Cache dentro do bloco de um Loop ou de um ResourceForEach, o salvamento é rejeitado. É porque o limite máximo de quantidade abaixo não impõe restrição alguma dentro de uma iteração. Como se grava um dado por volta, o número de Statements escrito na definição e o número de chaves de cache realmente usadas divergem.
  • Você pode colocar até 5 por definição (incluindo aninhados, somados independentemente da operação). Acima disso, o salvamento é rejeitado.
  • O valor guardado vai até 10.240 bytes (10KiB). Acima disso, esse Statement falha (status 422). É igual a qualquer outra falha em tempo de execução, portanto pode ser tratado localmente com Try/catch.
// reutiliza a taxa de câmbio por 30 segundos.
{ "type": "Cache", "action": "Get", "name": "cached", "key": "rates" }
 
// se houver um valor guardado, devolve-o tal como está, sem chamada externa
{ "type": "If", "condition": { "!!": [ "{ /cached }" ] },
  "then": [ { "type": "Return", "value": "{ /cached }" } ] }
 
{ "type": "Http", "name": "fetched", "method": "GET", "url": "https://api.example.com/rates" }
{ "type": "Cache", "action": "Set", "key": "rates", "value": "{ /fetched/body }", "ttl": 30 }
{ "type": "Return", "value": "{ /fetched/body }" }
 
// descarta o valor guardado antes de ele expirar
{ "type": "Cache", "action": "Delete", "key": "rates" }

Parse de valores

ParseJson

Faz o parse de um texto JSON no valor que ele representa e o vincula a um nome. Use para o corpo recebido de Http com responseType: "Text", para uma string JSON que chegou no payload, ou para JSON guardado como string em um campo. Não é uma chamada externa, portanto não é contado no limite de chamadas externas e também não declara tempo algum no orçamento de tempo.

CampoDescrição
nameO nome que guarda o valor parseado (obrigatório). Nos outros Statements é opcional, mas aqui é obrigatório. O Statement não faz nada além de vincular seu resultado, então um sem nome não tem efeito algum
valueO texto JSON a ser parseado (expressão de valor, obrigatório). Aponte para um valor de uma etapa anterior, como em { /resp/body }, ou escreva o texto JSON literalmente (uma { dentro do literal não é lida como template { ponteiro })
  • Resultado: o próprio valor parseado. Um objeto continua objeto, um array continua array, e um valor único como 42 ou "a" também é parseado. Depois você aponta para o interior com { /<name>/... }.
  • Um valor já parseado é vinculado sem alteração. Quando value resolve para algo que não é string, não há texto a parsear, então esse valor é vinculado como está.
  • Um { /pointer } dentro do texto parseado não é resolvido de novo. Mesmo que uma string recebida de fora contenha uma expressão como { /payload/... }, ela não é substituída por um valor e permanece texto.
  • null distingue dois casos. Se o texto a parsear for apenas a palavra null, isso é normal e o resultado também é null. Já se o lugar apontado por value estiver vazio, sem valor nenhum, não há nada a parsear e o Statement falha.
  • Falha: quando value resolve sem valor ou apenas com espaços, e quando o texto não é JSON. Trate com Try/catch como qualquer outra falha em tempo de execução; a mensagem de erro leva o texto que se tentou parsear.
  • Conta como um Statement no número de Statements por definição, mas não tem relação com o limite de chamadas externas nem com o limite de SetVar.
// 1) Uma API que não devolve JSON: receber como Text e fazer o parse
{ "type": "Http", "method": "GET", "url": "https://api.partner.example/v1/quote",
  "responseType": "Text", "name": "resp" },
{ "type": "ParseJson", "name": "quote", "value": "{ /resp/body }" },
 
// 2) Fazer o parse de uma string JSON que chegou no payload
{ "type": "ParseJson", "name": "spec", "value": "{ /payload/fields/specJson }" }

Verificação de assinatura e tratamento de texto

São os Statements que conferem a assinatura que uma processadora de pagamentos enviou por webhook e que desempacotam a string na qual essa assinatura vem embalada. Os três são cálculo, não chamada externa, portanto não são contados no limite de chamadas externas e também não declaram tempo algum no orçamento de tempo; e, como não têm posição de dados, não têm relação com a regra do prefixo $. Um exemplo completo combinando os três está na verificação de assinatura de webhook do cookbook.

Os três Statements têm um limite máximo para o comprimento do valor resolvido. Não é o comprimento da expressão, mas o comprimento do valor que ela aponta (os dezesseis caracteres de { /rawPayload } apontam para dezenas de KB) e, se exceder, a execução falha e pode ser tratada com Try. Os números estão reunidos em Limites de comprimento de valor.

Signature

Confere se o código de assinatura recebido é igual ao código gerado com secret e vincula essa resposta como um valor Boolean. A assinatura que uma processadora de pagamentos (PG, MoR) envia por webhook é verificada com este Statement.

CampoDescrição
nameO nome que guarda o resultado da verificação (obrigatório). { /<name> } é true ou false. Verificar e não usar o resultado equivale a não verificar, por isso não pode ser omitido
algorithmO hash com que o código é gerado (obrigatório). SHA1, SHA256, SHA384, SHA512
secretA chave secreta compartilhada com a outra parte (expressão de valor, obrigatório). Declare a chave em secrets e escreva-a como { /secrets/<key> }
secretEncodingEm que notação está a string de chave para a qual secret aponta. Utf8 (padrão, chave em texto), Hex, Base64. Deixar em texto uma chave emitida em hex ou base64 resulta em outra chave, de modo que um código plausível é gerado, mas nunca coincide
valueA mensagem sobre a qual o código é calculado (expressão de valor, obrigatório). Precisa ser literalmente igual aos bytes que a outra parte assinou, portanto normalmente é { /rawPayload }, ou esse valor com o timestamp que o provedor enviou junto no cabeçalho prefixado a ele
expectedO código que o chamador enviou (expressão de valor, obrigatório). Ex.: { /headers/x-signature }
  • Resultado: um Boolean. Depois, use { /<name> } tal como está na condição de um If.
  • Escreva value com /rawPayload, não com o /payload parseado. Transformar de novo em string o payload parseado normaliza espaços, notação numérica e escapes, e não devolve os bytes que a outra parte assinou (Raízes de contexto).
  • Não existe campo para indicar a notação de saída. algorithm fixa o comprimento em bytes do código, e hex e base64 de um mesmo comprimento não coincidem em comprimento de string, portanto o motor recupera os bytes sem que a outra parte informe qual notação usou. Pela mesma razão, ele também não distingue maiúsculas de minúsculas no hex, nem base64 de base64url (inclusive com ou sem padding).
  • A diferença entre falha e false está em quem fornece o valor.
    • Se expected não vier ou o código não coincidir, o resultado é apenas false, não é falha. Informar separadamente a ausência do cabeçalho e a divergência do código ensinaria a quem enviou qual dos dois estava errado.
    • Se value estiver vazio, o cálculo é feito com uma mensagem vazia. Um corpo vazio também é objeto de assinatura.
    • Se secret não vier ou não estiver na notação declarada por secretEncoding, é falha. Dos três casos, esta é a única entrada do próprio autor. A mensagem de falha não leva secret nem value.
  • O limite máximo de value é 65.536 caracteres (com base no valor resolvido). É um valor ajustado ao tamanho dos corpos de webhook que os provedores realmente enviam.
  • A comparação decide a igualdade dos valores em constant-time. Quantos bytes iniciais coincidiram não vaza pelo tempo de resposta.
  • A chave de assinatura não é escrita literalmente em secret. Declare-a em secrets da definição e escreva "{ /secrets/<key> }". Os valores de secrets são criptografados e armazenados com segurança pela WEEGLOO. secretEncoding indica em que notação está a string de chave armazenada em secrets.
// provedor que assina o corpo inteiro
{ "type": "Signature", "name": "verified", "algorithm": "SHA256",
  "secret": "{ /secrets/WEBHOOK_SIGNING_SECRET }", "value": "{ /rawPayload }",
  "expected": "{ /headers/x-webhook-signature }" }
 
// provedor que emite a chave em base64 (a string base64 fica guardada como está em secrets)
{ "type": "Signature", "name": "verified", "algorithm": "SHA256",
  "secret": "{ /secrets/WEBHOOK_SIGNING_KEY }", "secretEncoding": "Base64",
  "value": "{ /rawPayload }", "expected": "{ /headers/webhook-signature }" }

Hash

Faz o digest de value e o vincula como uma string na notação definida por encoding. Use-o para reproduzir esquemas de assinatura que, em vez de HMAC, "concatenam alguns campos com a chave secreta e calculam SHA256".

CampoDescrição
nameO nome que guarda o digest (obrigatório)
algorithmMD5, SHA1, SHA256, SHA384, SHA512 (obrigatório). MD5 existe para reproduzir esquemas antigos que o exigem, e não é um valor a escolher para uma assinatura nova
valueA mensagem a digerir (expressão de valor, obrigatório)
encodingA notação do resultado. Hex (padrão), HexUpper, Base64, Base64Url
  • Não existe campo secret. Dentro de value, escreva { /secrets/<key> } na posição em que aquele esquema coloca a chave. Cada esquema põe a chave no início, no fim ou no meio, e escrevendo assim é possível expressar qualquer uma dessas posições. Declare a chave antes em secrets.
  • Resultado: uma string. Para comparar com o código que a outra parte enviou, escreva { "==": [ "{ /<name> }", "{ /headers/... }" ] }. Essa comparação, diferente da comparação constant-time de Signature, é uma comparação de igualdade comum.
  • Se value resolver sem valor algum ou apenas com espaços, é falha (por ser uma expressão do próprio autor).
  • O limite máximo de value é 128 caracteres. Por ser a posição que guarda uns poucos campos concatenados, é muito mais estreito que o de Signature. Se você precisar calcular sobre o corpo inteiro de um webhook, use Signature.
// SHA256(número do pedido + valor + MERCHANT_KEY) em hex maiúsculo
{ "type": "Hash", "name": "expectedSign", "algorithm": "SHA256", "encoding": "HexUpper",
  "value": "{ /payload/orderId }{ /payload/amount }{ /secrets/MERCHANT_KEY }" }

Regex

Aplica pattern a value e vincula o que mode pediu. Como as expressões de valor não têm nenhum meio de cortar strings (há apenas cat, que concatena, e in, que verifica inclusão), use este Statement para desempacotar algo que chega com vários valores embalados em um só cabeçalho, como t=…,v1=….

CampoDescrição
nameO nome que guarda o resultado (obrigatório). Em Capture, os elementos são apontados como { /<name>/1 }
mode"Match" vincula como Boolean se houve correspondência; "Capture" vincula a primeira correspondência como array (obrigatório)
patternA expressão regular (obrigatório). Não é uma expressão de valor, mas um literal (veja abaixo). As flags são escritas dentro do padrão, como (?i). No máximo 128 caracteres; acima disso, o salvamento é rejeitado
valueO texto ao qual aplicar o padrão (expressão de valor, obrigatório). Se o valor resolvido passar de 10.240 caracteres (10KiB), a execução falha
  • Resultado: Match é Boolean; Capture é um array ou null. No array, o índice 0 é a correspondência inteira e, de 1 em diante, vêm os grupos de captura; um grupo que não participou é null (não uma string vazia, que significaria ter havido correspondência). Se o padrão não aparecer, Capture é null, e não um array vazio.
  • Os dois modos perguntam "o padrão aparece em algum lugar?". Se o texto inteiro precisar ser igual ao padrão, ancore-o com ^…$. A pergunta foi mantida igual nos dois para que os dois Statements, um verificando com Match e outro extraindo com Capture, não devolvam respostas diferentes.
  • pattern é um dos três campos deste motor que não são expressões de valor (os outros são a key de Cache e a color de PdfWatermark). Executar tal e qual um padrão vindo da requisição deixaria o chamador escolher a expressão a ser executada, e o backtracking das expressões regulares transformaria isso em um meio de negação de serviço. Por isso, um { /pointer } dentro do padrão também não é convertido em valor: passa a fazer parte do padrão, literalmente.
  • O padrão é compilado uma única vez, para toda a definição, quando a execução começa. Mesmo dentro de um Loop ou de um ResourceForEach, ele não é recompilado a cada iteração, e um padrão inutilizável falha antes que o primeiro Statement faça qualquer coisa (tratável com Try).
// desempacota "t=1492774577,v1=<64 caracteres hex>" em { /sig/1 } = timestamp, { /sig/2 } = código
{ "type": "Regex", "name": "sig", "mode": "Capture",
  "pattern": "^t=(\\d+),v1=([0-9a-f]{64})$", "value": "{ /headers/x-provider-signature }" }
 
// verifica apenas o formato
{ "type": "Regex", "name": "isOrderId", "mode": "Match",
  "pattern": "^ORD-\\d{8}-\\d{4}$", "value": "{ /payload/orderId }" }

Controle de fluxo

If

Uma ramificação condicional. condition é JsonLogic, e verdadeiro/falso segue as regras de Avaliação de verdadeiro e falso.

CampoDescrição
conditionJsonLogic (avaliado como booleano)
thenO array de Statement a executar quando verdadeiro
else(Opcional) O array de Statement a executar quando falso
{ "type": "If",
  "condition": { "<": [ "{ /wallet/fields/balance/en-US }", "{ /payload/fields/cost }" ] },
  "then": [ { "type": "Return", "value": { "ok": false, "reason": "insufficient credit" } } ],
  "else": [ /* ... */ ] }

Loop

Iteração. Escolha um modo: over (foreach), while (condição) ou for (contado). Em qualquer modo, o motor impõe um limite máximo de iterações (para evitar loops infinitos). Esse limite é declarado em maxIterations e, se você não o escrever, aplica-se o limite máximo da plataforma. Dentro de body você também pode incluir chamadas externas (Http, EmailSend) e ingestão de arquivo de Media, e os Statements de chamada externa são de fato chamados a cada iteração na execução. O limite de número máximo de chamadas externas por definição continua valendo.

No orçamento de tempo, ele entra como multiplicação. O tempo que este Statement declara é o tempo declarado por body multiplicado por maxIterations (10.000 se não houver) (veja Orçamento de tempo). Se não houver chamada externa em body, o tempo declarado é 0, portanto o orçamento base de 30 segundos é o limite efetivo.

CampoDescrição
overforeach: uma expressão de valor que resolve para um array
whilecondição: JsonLogic (repete enquanto verdadeiro)
forcontado: { "from", "to", "step"? }. De from até to inclusive; step tem padrão 1
maxIterationsO número máximo de iterações (opcional). Se você não o escrever, aplica-se o limite máximo da plataforma, 10.000, e um valor maior que esse é recusado ao salvar
name(Opcional) O nome ao qual vincular o item atual (foreach) ou o índice (while, for) ({ /<name> })
bodyO array de Statement do corpo do loop
// foreach
{ "type": "Loop", "over": "{ /payload/fields/items }", "name": "item", "maxIterations": 100,
  "body": [ { "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_item" } },
             "fields": { "name": { "en-US": "{ /item/name }" } } } ] }
 
// while
{ "type": "Loop", "while": "{ /vars/hasMore }", "maxIterations": 1000, "body": [ /* ... */ ] }
 
// counted (1..10 step 2)
{ "type": "Loop", "for": { "from": 1, "to": 10, "step": 2 }, "name": "i", "maxIterations": 100, "body": [ /* ... */ ] }

Parallel

Executa as branches de forma concorrente e prossegue após a junção delas. Referências entre branches não são permitidas (se houver uma dependência, coloque-as sequencialmente).

CampoDescrição
branchesStatement[][]. Cada elemento é uma branch (um array de Statements)
{ "type": "Parallel", "branches": [
  [ { "type": "Http", "method": "GET", "url": "https://api.a.com/x", "name": "a" } ],
  [ { "type": "Http", "method": "GET", "url": "https://api.b.com/y", "name": "b" } ]
] }

Return

É o return da programação comum. Retorna o resultado do Script ao chamador e encerra normalmente naquele ponto.

CampoDescrição
value(Opcional) A expressão de valor a retornar
isErrorPadrão false. Quando true, value retorna como o error da resposta (caso contrário, como return)
statusCodeO código de status da resposta. Padrão 200
  • Se Return nunca for alcançado, não há valor de retorno. Para retornar um resultado, especifique value explicitamente.
  • Por ser um encerramento normal, não uma exceção ou throw, não é alvo de catch (mesmo dentro de Try, encerra o Script inteiro, mas finally ainda é executado).
  • Um guard também é expresso com este Statement. Se você colocar um Return no then de um If, ele retorna um valor quando a condição é violada e não executa os Statements seguintes. Este é um dos vários usos de Return.
{ "type": "Return", "value": { "orderId": "{ /order/sys/id }", "status": "paid" }, "statusCode": 201 }
{ "type": "Return", "value": { "reason": "payment failed" }, "isError": true, "statusCode": 402 }

Try

Tratamento de exceções.

CampoDescrição
bodyO array de Statement a tentar
catch(Opcional) Executa quando body falha. Expõe { message } em /error (em qual Statement a falha ocorreu não é incluído)
finally(Opcional) Executa sempre, independentemente de sucesso ou falha
  • Se catch o tratar, o Script não é interrompido. Apenas uma falha sem catch interrompe o Script (incluindo uma tentativa de compensação).
  • O que conta como "falha" e os limites da compensação são abordados em Semântica de execução, restrições e segurança.
{ "type": "Try",
  "body":    [ { "type": "Http", "method": "POST", "url": "https://primary.api/gen", "name": "resp" },
               { "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_result" } },
                 "fields": { "text": { "en-US": "{ /resp/body/text }" } } } ],
  "catch":   [ { "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_result" } },
                 "fields": { "text": { "en-US": "Generation failed" }, "error": { "en-US": "{ /error/message }" } } } ],
  "finally": [ /* sempre é executado */ ] }