Skip to content

Custom actions

This content is not available in your language yet.

A custom action is a named, reusable REST call to your own system, for example “create a ticket” in your ticketing backend. A workflow action node calls it with arguments rendered from the conversation. Every operation here takes a dashboard JWT. A stored bearerToken is kept as a secret and never returned; it is sent as Authorization: Bearer <token>, or under the header you name in credentialSecretHeaderName for APIs that use a static secret header.

Every operation is a POST of { "query", "variables" } to https://api.wetel.dev/graphql (sandbox) with the x-huat-platform: customer header and the credential shown. New here? Start with the Quickstart and the Authentication cheat sheet. Errors are listed in Errors; Postman and OpenAPI files are on Downloads.

OperationKindCredentialWhat it does
createCustomActionmutationDashboard JWTRegister a REST call for workflow action nodes.
customActionsqueryDashboard JWTList the workspace’s custom actions.
testCustomActionmutationDashboard JWTMake one real call of a custom action and classify the answer, before a conversation depends on it.
deleteCustomActionmutationDashboard JWTDelete a custom action. Action nodes that use it fail when they run.

Register a REST call for workflow action nodes.

  • Kind: mutation
  • Credential: Dashboard JWT: Authorization: Bearer <jwt>, from login (a person or a one-off script)
  • Available in: sandbox and production
  • method is one of GET, POST, PUT, PATCH, DELETE (default POST).
  • argsSchema documents the arguments the call expects; it is not enforced at run time.
  • Limited to 10 calls per minute per client IP.

Request

mutation CreateAction($input: CreateCustomActionInput!) {
createCustomAction(input: $input) {
id
name
method
url
credentialSecretHeaderName
}
}
variables
{
"input": {
"name": "create_ticket",
"description": "Create a ticket in the ticketing backend and return its number.",
"url": "https://tickets.example.com/api/tickets",
"method": "POST",
"bearerToken": "example-backend-token-not-real",
"argsSchema": {
"type": "object",
"properties": {
"subject": {
"type": "string"
},
"level": {
"type": "string",
"enum": [
"L1",
"L2",
"L3"
]
}
},
"required": [
"subject",
"level"
]
}
}
}

Input fields

CreateCustomActionInput

FieldTypeRequiredDescription
argsSchemaJSONnoOptional JSON Schema (an object, send it through variables) documenting the arguments; not enforced on calls.
bearerTokenStringnoOptional static secret, sent as Authorization: Bearer (or under credentialSecretHeaderName). Stored as a secret and never returned. Omit for an endpoint that needs no credential.
credentialSecretHeaderNameStringnoOptional header name to send the stored bearerToken under, for third-party APIs that authenticate with a static non-Bearer secret header (e.g. ‘X-Client-Secret’, ‘X-Api-Key’). Omit for the default ‘Authorization: Bearer <token>’ behavior. When set, the raw secret value is sent under this header with no ‘Bearer ’ prefix, and no Authorization header is sent. Only accepted when bearerToken is supplied in the same call.
credentialSourceStringnoOptional SESSION_ value that resolves the credential live, per call, from the end user signed in to the conversation through single sign-on, for the products that support it; an invalid value is refused with the list of allowed values. Leave unset for bearerToken or Lark app credentials.
creditsPerCallIntnoThe credits charged for one successful call of this action (an integer, 0 or more). Omit to use the platform default (2 credits).
descriptionStringnoOptional free-text description.
extraHeadersJSONnoStatic, non-secret headers sent on every call: a JSON object of header name to string value (send it through variables). Never a credential; it is returned as is.
larkAppIdStringnoApp id of your own Lark (or Feishu) app; give it together with larkAppSecret instead of bearerToken. The action then authenticates with that app’s tenant access token, fetched by the server.
larkAppSecretStringnoApp secret of your own Lark (or Feishu) app, given with larkAppId. Stored as a secret and never returned.
larkRegionStringnoWhich platform the Lark app lives on: FEISHU or LARK_SUITE (default LARK_SUITE).
methodStringnoGET, POST, PUT, PATCH or DELETE (default POST).
nameString!yesDisplay name (1 to 100 characters).
urlString!yesURL to call. Private and internal addresses are refused, and redirects are not followed.

Code samples

createCustomAction.sh
#!/usr/bin/env bash
# createCustomAction: Register a REST call for workflow action nodes.
# Credential: Dashboard JWT (WETEL_JWT).
API_URL="${WETEL_API_URL:-https://api.wetel.dev/graphql}"
curl -sS "$API_URL" \
-H "Content-Type: application/json" \
-H "x-huat-platform: customer" \
-H "Authorization: Bearer $WETEL_JWT" \
--data-binary @- <<'JSON'
{
"query": "mutation CreateAction($input:CreateCustomActionInput!){createCustomAction(input:$input){id name method url credentialSecretHeaderName}}",
"variables": {
"input": {
"name": "create_ticket",
"description": "Create a ticket in the ticketing backend and return its number.",
"url": "https://tickets.example.com/api/tickets",
"method": "POST",
"bearerToken": "example-backend-token-not-real",
"argsSchema": {
"type": "object",
"properties": {
"subject": {
"type": "string"
},
"level": {
"type": "string",
"enum": [
"L1",
"L2",
"L3"
]
}
},
"required": [
"subject",
"level"
]
}
}
}
}
JSON

Responses

Success (HTTP 200)

{
"data": {
"createCustomAction": {
"id": 9,
"name": "create_ticket",
"method": "POST",
"url": "https://tickets.example.com/api/tickets",
"credentialSecretHeaderName": null
}
}
}
BAD_REQUEST: Header name without a secret (HTTP 200)
{
"errors": [
{
"message": "credentialSecretHeaderName requires bearerToken in the same call — a header name with no secret to send under it has no effect.",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"createCustomAction"
],
"extensions": {
"code": "BAD_REQUEST",
"originalError": {
"message": "credentialSecretHeaderName requires bearerToken in the same call — a header name with no secret to send under it has no effect.",
"error": "Bad Request",
"statusCode": 400
}
}
}
],
"data": null
}
BAD_REQUEST: Unsupported method (HTTP 200)
{
"errors": [
{
"message": "method must be one of the following values: GET, POST, PUT, PATCH, DELETE",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"createCustomAction"
],
"extensions": {
"code": "BAD_REQUEST",
"originalError": {
"message": "method must be one of the following values: GET, POST, PUT, PATCH, DELETE",
"error": "Bad Request",
"statusCode": 400
}
}
}
],
"data": null
}

List the workspace’s custom actions.

  • Kind: query
  • Credential: Dashboard JWT: Authorization: Bearer <jwt>, from login (a person or a one-off script)
  • Available in: sandbox and production

Request

query ListActions {
customActions {
id
name
method
url
description
creditsPerCall
}
}
variables
{}

Code samples

customActions.sh
#!/usr/bin/env bash
# customActions: List the workspace's custom actions.
# Credential: Dashboard JWT (WETEL_JWT).
API_URL="${WETEL_API_URL:-https://api.wetel.dev/graphql}"
curl -sS "$API_URL" \
-H "Content-Type: application/json" \
-H "x-huat-platform: customer" \
-H "Authorization: Bearer $WETEL_JWT" \
--data-binary @- <<'JSON'
{
"query": "query ListActions{customActions{id name method url description creditsPerCall}}",
"variables": {}
}
JSON

Responses

Success (HTTP 200)

{
"data": {
"customActions": [
{
"id": 9,
"name": "create_ticket",
"method": "POST",
"url": "https://tickets.example.com/api/tickets",
"description": "Create a ticket in the ticketing backend and return its number.",
"creditsPerCall": null
}
]
}
}
UNAUTHENTICATED: Called with an API key instead of a JWT (HTTP 200)
{
"errors": [
{
"message": "Unauthorized",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"customActions"
],
"extensions": {
"code": "UNAUTHENTICATED",
"originalError": {
"message": "Unauthorized",
"statusCode": 401
}
}
}
],
"data": null
}

Make one real call of a custom action and classify the answer, before a conversation depends on it.

  • Kind: mutation
  • Credential: Dashboard JWT: Authorization: Bearer <jwt>, from login (a person or a one-off script)
  • Available in: sandbox and production
  • The call really happens, through the same path a workflow uses. An action whose method is not GET needs confirmWrite: true and may change data on the target.
  • sampleArgs (a flat JSON object) is sent as the body, or as the query string for GET and DELETE, exactly as written: template syntax such as {{x}} is refused.
  • outcome tells you what to fix: CREDENTIAL_REJECTED (401/403: the stored token or its header is wrong), REQUEST_REJECTED (other 4xx), SERVER_ERROR (5xx), TIMEOUT, UNREACHABLE, BLOCKED (a private or reserved address), WRITE_NOT_CONFIRMED. The response snippet is masked and cut to 500 characters.
  • A successful test is billed like the same call in a workflow (creditsBilled); a failed or refused test is free. Limited to 10 calls per minute per client IP.

Request

mutation TestAction($input: TestCustomActionInput!) {
testCustomAction(input: $input) {
outcome
httpStatus
message
responseSnippet
durationMs
creditsBilled
}
}
variables
{
"input": {
"id": 9,
"confirmWrite": true,
"sampleArgs": {
"subject": "Test ticket from the developer portal",
"level": "L1"
}
}
}

Input fields

TestCustomActionInput

FieldTypeRequiredDescription
confirmWriteBooleannoRequired to test any action whose method is not GET, because the call really happens and may change data on the target. Default false.
idInt!yesThe custom action to test.
sampleArgsJSONnoA flat JSON object sent as the request body (or the query string for GET/DELETE), exactly as a workflow node would send its rendered arguments. Values are sent verbatim: template syntax such as {{x}} is rejected. Omit to send no arguments.

Allowed values

  • CustomActionTestOutcome: BLOCKED, CREDENTIAL_REJECTED, CREDENTIAL_UNAVAILABLE, NOT_TESTABLE, OK, REQUEST_REJECTED, SERVER_ERROR, TIMEOUT, UNKNOWN_ERROR, UNREACHABLE, WRITE_NOT_CONFIRMED

Code samples

testCustomAction.sh
#!/usr/bin/env bash
# testCustomAction: Make one real call of a custom action and classify the answer, before a conversation depends on it.
# Credential: Dashboard JWT (WETEL_JWT).
API_URL="${WETEL_API_URL:-https://api.wetel.dev/graphql}"
curl -sS "$API_URL" \
-H "Content-Type: application/json" \
-H "x-huat-platform: customer" \
-H "Authorization: Bearer $WETEL_JWT" \
--data-binary @- <<'JSON'
{
"query": "mutation TestAction($input:TestCustomActionInput!){testCustomAction(input:$input){outcome httpStatus message responseSnippet durationMs creditsBilled}}",
"variables": {
"input": {
"id": 9,
"confirmWrite": true,
"sampleArgs": {
"subject": "Test ticket from the developer portal",
"level": "L1"
}
}
}
}
JSON

Responses

OK (HTTP 200)

{
"data": {
"testCustomAction": {
"outcome": "OK",
"httpStatus": 200,
"message": "The target answered successfully.",
"responseSnippet": "{\"ticketNumber\":\"T-1003\"}",
"durationMs": 284,
"creditsBilled": 2
}
}
}

Write not confirmed (a classified outcome, not an error) (HTTP 200)

{
"data": {
"testCustomAction": {
"outcome": "WRITE_NOT_CONFIRMED",
"httpStatus": null,
"message": "This action uses POST, so the test call would really be made and may change data on the target. Send confirmWrite: true to run it.",
"responseSnippet": null,
"durationMs": 0,
"creditsBilled": 0
}
}
}
BAD_REQUEST: Template syntax in sampleArgs (HTTP 200)
{
"errors": [
{
"message": "sampleArgs are sent verbatim and may not contain template syntax ({{ }}).",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"testCustomAction"
],
"extensions": {
"code": "BAD_REQUEST",
"originalError": {
"message": "sampleArgs are sent verbatim and may not contain template syntax ({{ }}).",
"error": "Bad Request",
"statusCode": 400
}
}
}
],
"data": null
}
NOT_FOUND: Not in your workspace (HTTP 200)
{
"errors": [
{
"message": "Custom action #9 not found",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"testCustomAction"
],
"extensions": {
"code": "NOT_FOUND",
"status": 404,
"originalError": {
"message": "Custom action #9 not found",
"error": "Not Found",
"statusCode": 404
}
}
}
],
"data": null
}

Delete a custom action. Action nodes that use it fail when they run.

  • Kind: mutation
  • Credential: Dashboard JWT: Authorization: Bearer <jwt>, from login (a person or a one-off script)
  • Available in: sandbox and production
  • Limited to 10 calls per minute per client IP.

Request

mutation DeleteAction($id: Int!) {
deleteCustomAction(id: $id)
}
variables
{
"id": 9
}

Code samples

deleteCustomAction.sh
#!/usr/bin/env bash
# deleteCustomAction: Delete a custom action. Action nodes that use it fail when they run.
# Credential: Dashboard JWT (WETEL_JWT).
API_URL="${WETEL_API_URL:-https://api.wetel.dev/graphql}"
curl -sS "$API_URL" \
-H "Content-Type: application/json" \
-H "x-huat-platform: customer" \
-H "Authorization: Bearer $WETEL_JWT" \
--data-binary @- <<'JSON'
{
"query": "mutation DeleteAction($id:Int!){deleteCustomAction(id:$id)}",
"variables": {
"id": 9
}
}
JSON

Responses

Success (HTTP 200)

{
"data": {
"deleteCustomAction": true
}
}
NOT_FOUND: Not in your workspace (HTTP 200)
{
"errors": [
{
"message": "Custom action #9 not found",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"deleteCustomAction"
],
"extensions": {
"code": "NOT_FOUND",
"status": 404,
"originalError": {
"message": "Custom action #9 not found",
"error": "Not Found",
"statusCode": 404
}
}
}
],
"data": null
}