Skip to content

MCP Connectors & Custom Actions API

This content is not available in your language yet.

This page documents every GraphQL operation for managing MCP connectors and custom actions — the two ways a workflow’s tool/ACTION nodes reach out to third-party systems. For the conceptual model (what MCP connectors are, how they differ from custom actions, and known limitations), see MCP Connectors. This page is strictly the field-by-field CRUD reference.

All operations on this page require JWT dashboard authentication (AuthJwtGuard) — none of them are reachable with an API key. Every request must also include the x-huat-platform: customer header. See API Reference Overview for general request conventions, Agents API for agent management, and Workflows API for how a connector or action gets referenced by a workflow node.

An MCP connector represents a registered third-party MCP tool server — an endpoint plus credential that a workflow’s tool node can call.

Fetches a single MCP connector by id.

Auth: JWT dashboard auth only.

NameTypeRequiredDescription
idInt!YesThe connector’s id.

McpConnectorDto (nullable).

FieldTypeDescription
idInt!Connector id.
nameString!Display name.
serverUrlString!The MCP server’s endpoint URL.
createdAtDateTime!Creation timestamp.
updatedAtDateTime!Last update timestamp.

Note the API key used to authenticate to the third-party server is write-only — it’s accepted on create/update but never returned in any query response.

query GetMcpConnector($id: Int!) {
mcpConnector(id: $id) {
id
name
serverUrl
}
}
{ "id": 7 }

Headers:

Authorization: Bearer <your-jwt>
x-huat-platform: customer
{
"data": {
"mcpConnector": {
"id": 7,
"name": "Library Catalog",
"serverUrl": "https://tools.example.com/mcp"
}
}
}

Lists every MCP connector registered for the caller’s tenant.

Auth: JWT dashboard auth only.

None.

[McpConnectorDto!]! — same shape as mcpConnector above.

query ListMcpConnectors {
mcpConnectors {
id
name
serverUrl
}
}

Headers:

Authorization: Bearer <your-jwt>
x-huat-platform: customer
{
"data": {
"mcpConnectors": [
{
"id": 7,
"name": "Library Catalog",
"serverUrl": "https://tools.example.com/mcp"
},
{
"id": 9,
"name": "Ticketing System",
"serverUrl": "https://ticketing.example.com/mcp"
}
]
}
}

Lists the tool schemas exposed by a given MCP connector — what an agent/workflow using this connector can actually call. This is the recommended way to discover exactly what a tool workflow node pointed at a given connector can do, rather than guessing tool names and argument shapes from documentation or trial and error.

Auth: JWT dashboard auth only.

NameTypeRequiredDescription
connectorIdInt!YesThe MCP connector to introspect.

[McpToolSchemaDto!]!

FieldTypeDescription
nameString!Tool name, as exposed by the MCP server.
descriptionStringHuman-readable description of what the tool does.
inputSchemaJSONThe tool’s JSON Schema for its input arguments.
query ListMcpTools($connectorId: Int!) {
listMcpTools(connectorId: $connectorId) {
name
description
inputSchema
}
}
{ "connectorId": 7 }

Headers:

Authorization: Bearer <your-jwt>
x-huat-platform: customer
{
"data": {
"listMcpTools": [
{
"name": "search_catalog",
"description": "Search the library catalog by title, author, or keyword.",
"inputSchema": {
"type": "object",
"properties": {
"query": { "type": "string" },
"limit": { "type": "integer", "default": 10 }
},
"required": ["query"]
}
},
{
"name": "get_availability",
"description": "Check whether a specific item is currently available to borrow.",
"inputSchema": {
"type": "object",
"properties": { "itemId": { "type": "string" } },
"required": ["itemId"]
}
}
]
}
}

Use the returned name values and inputSchema shapes when configuring a tool node in a workflow — see Workflows Overview for how tool nodes reference a connector and tool name.

Registers a new MCP connector (third-party tool server credential/endpoint) for the caller’s tenant.

Auth: JWT dashboard auth only.

NameTypeRequiredDescription
input.nameString!YesDisplay name for the connector.
input.serverUrlString!YesThe MCP server’s endpoint URL.
input.apiKeyString!YesCredential used to authenticate to the MCP server. Stored securely; never returned by any query.

McpConnectorDto! — the newly created connector (without the API key).

mutation CreateMcpConnector($input: CreateMcpConnectorInput!) {
createMcpConnector(input: $input) {
id
name
serverUrl
}
}
{
"input": {
"name": "Library Catalog",
"serverUrl": "https://tools.example.com/mcp",
"apiKey": "REPLACE_WITH_YOUR_MCP_SERVER_API_KEY"
}
}

Headers:

Authorization: Bearer <your-jwt>
x-huat-platform: customer
{
"data": {
"createMcpConnector": {
"id": 7,
"name": "Library Catalog",
"serverUrl": "https://tools.example.com/mcp"
}
}
}

Updates an MCP connector owned by the caller’s tenant.

Auth: JWT dashboard auth only.

NameTypeRequiredDescription
input.idInt!YesConnector to update.
input.nameStringNoNew display name.
input.serverUrlStringNoNew endpoint URL.

Note apiKey cannot be changed via updateMcpConnector — it is only accepted on create. To rotate the credential, delete and re-create the connector (which also means updating any workflow tool node that referenced the old connector id).

McpConnectorDto! — the updated connector.

mutation UpdateMcpConnector($input: UpdateMcpConnectorInput!) {
updateMcpConnector(input: $input) {
id
name
serverUrl
}
}
{
"input": {
"id": 7,
"name": "Library Catalog v2",
"serverUrl": "https://tools-v2.example.com/mcp"
}
}

Headers:

Authorization: Bearer <your-jwt>
x-huat-platform: customer
{
"data": {
"updateMcpConnector": {
"id": 7,
"name": "Library Catalog v2",
"serverUrl": "https://tools-v2.example.com/mcp"
}
}
}

Deletes an MCP connector owned by the caller’s tenant.

Auth: JWT dashboard auth only.

NameTypeRequiredDescription
idInt!YesConnector to delete.

Boolean!true on success.

Deleting a connector that’s still referenced by a published workflow’s tool node will break that node at runtime — check listMcpTools/your workflow graphs before deleting a connector that may be in active use.

mutation DeleteMcpConnector($id: Int!) {
deleteMcpConnector(id: $id)
}
{ "id": 9 }

Headers:

Authorization: Bearer <your-jwt>
x-huat-platform: customer
{ "data": { "deleteMcpConnector": true } }

A custom action is a named, reusable REST call definition — URL, HTTP method, optional bearer-token credential, and optional extra static headers — for use by ACTION workflow nodes. Use custom actions for simple REST integrations that don’t warrant standing up a full MCP server.

Every custom action’s credentialSource field (returned on CustomActionDto) records which mechanism authenticates its outgoing calls. It’s read-only for most values — set indirectly by which other input fields you supply, or by Wetel provisioning — with one exception: SESSION_OMNICHAT/SESSION_AISCRM/SESSION_NORTIA can be set directly via a credentialSource input field on createCustomAction/updateCustomAction.

ValueSet viaWho can create one
nullcreateCustomAction with bearerTokenAny tenant, self-serve — a plain static bearer token.
"LARK_TENANT"createCustomAction with larkAppId/larkAppSecretAny tenant, self-serve — see Using Lark above.
"OMNICHAT" / "NORTIA" / "AISCRM"Not exposed on createCustomAction todayWetel-provisioned only — a tenant-wide shared account credential for one of Wetel’s certified partners.
"SESSION_OMNICHAT" / "SESSION_AISCRM" / "SESSION_NORTIA"createCustomAction/updateCustomAction with input.credentialSource set to that value directlyAny tenant, self-serve — see Sessions API: sdkVerifyWebbyxOneIdentity.

Setting credentialSource to SESSION_OMNICHAT/SESSION_AISCRM/SESSION_NORTIA doesn’t store a credential on the action itself — it opts the action into resolving its credential live, per call, from whichever end user is currently signed in to that conversation via WebbyX One (see the mutation itself below). It’s mutually exclusive with bearerToken/larkAppId+larkAppSecret — supplying both is rejected. credentialSource is the only field on either input that accepts one of these three values directly; "OMNICHAT", "NORTIA", "AISCRM", and "LARK_TENANT" remain rejected if passed here.

The three self-serve paths that matter most in practice today: a plain bearer token or your own Lark app cover any REST endpoint you control. For calling OmniChat, aisCRM, or Nortia, the realistic self-serve path is SESSION_OMNICHAT/SESSION_AISCRM/SESSION_NORTIA — create (or update) a custom action with credentialSource set to one of those three values, then have your end users sign in via WebbyX One and link their product identity (sdkVerifyWebbyxOneIdentity’s productTickets), rather than asking Wetel to provision a tenant-wide shared account.

This matters directly for the certified operation catalog — a certified operation’s compatibleCredentialSources names which of these values it’s meant to pair with, and the action node it’s wired to must actually carry one of them.

As of 2026-09-08, url and method are immutable on an action whose credentialSource is a Wetel-provisioned shared credential ("OMNICHAT" / "NORTIA" / "AISCRM" / "LARK_TENANT") — updateCustomAction rejects any attempt to change either field, telling you to create a new action instead. This prevents such an action from being repointed at an arbitrary endpoint while still sending Wetel’s own shared partner credential on every call. SESSION_OMNICHAT/SESSION_AISCRM/SESSION_NORTIA and a plain bearerToken-credentialed action are unaffected — url/method stay freely editable on those.

By default, a custom action’s stored bearerToken renders as Authorization: Bearer <token> on every call. Some third-party APIs instead authenticate with a static, differently-named secret header — Nortia’s REST Integration API uses X-Client-Secret; X-Api-Key is common generally. credentialSecretHeaderName on createCustomAction/updateCustomAction covers this: set it to the header name the target API expects, and the stored secret is sent under that header instead, as the raw value with no Bearer prefix, and no Authorization header is sent at all for that call.

It only applies to the plain self-service bearerToken credential (credentialSource: null). It has no effect on any Wetel-provisioned shared credential ("OMNICHAT"/"NORTIA"/"AISCRM"/"LARK_TENANT") or session-identity source ("SESSION_OMNICHAT"/"SESSION_AISCRM"/"SESSION_NORTIA") — each of those builds its own Authorization: Bearer header server-side and never reads this field.

Create-time rule: credentialSecretHeaderName is rejected unless bearerToken is supplied in the same call — a header name with no secret to send under it would be a silent no-op.

mutation CreateActionWithCustomHeader($input: CreateCustomActionInput!) {
createCustomAction(input: $input) {
id
name
credentialSecretHeaderName
}
}
{
"input": {
"name": "Partner API (X-Api-Key auth)",
"url": "https://api.partner.example/v1/records",
"method": "POST",
"bearerToken": "REPLACE_WITH_YOUR_API_KEY",
"credentialSecretHeaderName": "X-Api-Key"
}
}

Update-time rule: on an existing action, credentialSecretHeaderName can be changed to a different header name, or cleared back to the default Authorization: Bearer behavior by passing null. It’s rejected if the action has no stored bearerToken at all, or if the action’s credentialSource is one of the Wetel-provisioned/session-identity values above (those never consult this field).

extraHeaders exists for a target API that needs more than one static header alongside Authorization (or the custom credential header above) — the motivating case is a partner API requiring both a credential header and a separate, non-secret identifier header like x-organization-id: <org uid>. It’s a plain { [key: string]: string } map merged into every outgoing request’s headers first; the credential header (Authorization: Bearer <token> by default, or <credentialSecretHeaderName>: <token> if set) is applied afterward and wins on a same-named collision.

extraHeaders is not a secret-storage mechanism, and this is worth repeating: never put a secret in it. It’s stored and returned as plain JSON — anyone who can query customActions for your tenant sees these values verbatim, unlike bearerToken, which is write-only and never returned by any query. It exists for non-sensitive request metadata a target API requires, not for credentials. Before adding a header here, ask: would I be comfortable with a tenant admin viewing this value in a query response? If the answer is no — an API key, a token, anything that must stay secret — it does not belong in extraHeaders. Use bearerToken (with credentialSecretHeaderName if the target needs a non-Authorization header) for the one credential a custom action does support; a target API needing more than one secret header isn’t a fit for a custom action today.

Custom actions have first-class support for Lark (Feishu internationally) — send messages, create/query Base records, route approvals, and more, all via Lark’s Open Platform REST API, using your own Lark app, not a shared or third-party credential.

Step 1 — create a Lark app. In Lark’s Developer Console (or open.feishu.cn if your org is on Feishu), create a custom app and note its App ID and App Secret from the app’s “Credentials & Basic Info” page.

Step 2 — register a custom action with larkAppId/larkAppSecret/larkRegion instead of bearerToken:

mutation CreateLarkAction($input: CreateCustomActionInput!) {
createCustomAction(input: $input) {
id
name
url
method
credentialSource
}
}
{
"input": {
"name": "Send Lark Message",
"url": "https://open.larksuite.com/open-apis/im/v1/messages?receive_id_type=open_id",
"method": "POST",
"larkAppId": "cli_xxxxxxxxxxxxxxxx",
"larkAppSecret": "REPLACE_WITH_YOUR_LARK_APP_SECRET",
"larkRegion": "LARK_SUITE"
}
}

Wetel handles the rest: it exchanges your app credentials for a Lark tenant_access_token (Lark’s own short-lived, ~2-hour token) on every call, refreshing it automatically before it expires. Your ACTION workflow node just references this custom action’s id — no token management on your side.

larkAppId/larkAppSecret are write-only, same as bearerToken — accepted on createCustomAction, never returned by customActions or any other query. Supply either bearerToken OR larkAppId+larkAppSecret, never both — the mutation rejects the request if both are present, since they resolve to different (mutually exclusive) credential mechanisms server-side.

larkAppId/larkAppSecret must be supplied together — providing one without the other is rejected. larkRegion is optional and defaults to LARK_SUITE (international) if you don’t set it; use FEISHU if your Lark org is on Feishu’s China deployment.

A few real Lark Open Platform endpoints to build against, each its own createCustomAction call:

ActionEndpointNotes
Send a messagePOST /open-apis/im/v1/messagesTo a user, group, or your app’s own bot chat.
Create/query a Base recordPOST/GET /open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/recordsLark Base is Airtable-like structured data — a lightweight CRM target.
Create an approval instancePOST /open-apis/approval/v4/instancesRoute something to a human for sign-off.
Look up a user by email/mobilePOST /open-apis/contact/v3/users/batch_get_idUsually a prerequisite before sending a message — Lark’s messaging API needs an internal user id, not an email.

See Lark’s Open Platform API reference for the full endpoint list and request/response shapes — Wetel is a transport for these calls, not a wrapper around them, so any endpoint your app is authorized for works the same way.

Lists every custom action registered for the caller’s tenant.

Auth: JWT dashboard auth only.

None.

[CustomActionDto!]!

FieldTypeDescription
idInt!Action id.
nameString!Display name.
descriptionStringOptional description.
urlString!Target REST endpoint.
methodString!HTTP method (e.g. GET, POST).
argsSchemaJSONJSON Schema describing the arguments an ACTION node collects/passes for this call.
extraHeadersJSONAdditional static headers (a plain { [key: string]: string } map) sent on every call to this action, merged with (and never overriding) the credential header built from the bearer token. See “Extra static headers” below.
credentialSecretHeaderNameStringHeader name the stored bearerToken is sent under, if set (e.g. "X-Api-Key"). null = the default Authorization: Bearer <token> behavior. See “Credential header name” below.
credentialSourceStringWhich mechanism authenticates this action’s calls — null, "LARK_TENANT", "OMNICHAT", "NORTIA", "AISCRM", "SESSION_OMNICHAT", "SESSION_AISCRM", or "SESSION_NORTIA". Not directly settable except via input.credentialSource (see below) for the three SESSION_* values — every other value is derived from other input fields or Wetel-provisioned. See Credential sources above.
createdAtDateTime!Creation timestamp.
updatedAtDateTime!Last update timestamp.

The bearer token (if set) is write-only — it’s accepted on create/update but never returned by this or any query. extraHeaders, unlike the bearer token, is not treated as a secret — it’s stored and returned as-is, so it round-trips back out of customActions/createCustomAction/updateCustomAction unchanged. See the callout below before putting anything in it.

query ListCustomActions {
customActions {
id
name
url
method
argsSchema
extraHeaders
}
}

Headers:

Authorization: Bearer <your-jwt>
x-huat-platform: customer
{
"data": {
"customActions": [
{
"id": 3,
"name": "Create Support Ticket",
"url": "https://api.example.com/tickets",
"method": "POST",
"argsSchema": {
"type": "object",
"properties": {
"subject": { "type": "string" },
"body": { "type": "string" }
},
"required": ["subject", "body"]
}
}
]
}
}

Registers a new custom action for the caller’s tenant, for use by ACTION workflow nodes.

Auth: JWT dashboard auth only.

NameTypeRequiredDescription
input.nameString!YesDisplay name.
input.urlString!YesTarget REST endpoint.
input.methodStringNoHTTP method (defaults applied server-side if omitted).
input.descriptionStringNoOptional description.
input.argsSchemaJSONNoJSON Schema describing the call’s arguments.
input.bearerTokenStringNoBearer credential sent with the request. Stored securely; never returned by any query. Mutually exclusive with larkAppId/larkAppSecret below.
input.credentialSecretHeaderNameStringNoSend the stored bearerToken under this header name instead of Authorization: Bearer, e.g. "X-Api-Key". See “Credential header name” below. Rejected unless bearerToken is supplied in the same call.
input.extraHeadersJSONNoAdditional static headers ({ [key: string]: string }), merged onto every call alongside the credential header. See “Extra static headers” below — never put a real credential in here.
input.larkAppIdStringNoYour own Lark app’s App ID. See “Using Lark” below. Must be supplied together with larkAppSecret.
input.larkAppSecretStringNoYour own Lark app’s App Secret. Stored securely; never returned by any query.
input.larkRegionStringNoFEISHU or LARK_SUITE — which Lark deployment your app is on. Defaults to LARK_SUITE if omitted.
input.credentialSourceStringNoSet to "SESSION_OMNICHAT", "SESSION_AISCRM", or "SESSION_NORTIA" to opt this action into the session-identity-driven credential flow instead of a stored bearer token — see Credential sources above. Any other value ("OMNICHAT", "NORTIA", "AISCRM", "LARK_TENANT") is rejected here. Mutually exclusive with bearerToken/larkAppId+larkAppSecret.

CustomActionDto! — the newly created action (without the bearer token).

mutation CreateCustomAction($input: CreateCustomActionInput!) {
createCustomAction(input: $input) {
id
name
url
method
extraHeaders
}
}
{
"input": {
"name": "Create Support Ticket",
"url": "https://api.example.com/tickets",
"method": "POST",
"description": "Opens a new support ticket in the external helpdesk.",
"argsSchema": {
"type": "object",
"properties": {
"subject": { "type": "string" },
"body": { "type": "string" }
},
"required": ["subject", "body"]
},
"bearerToken": "REPLACE_WITH_YOUR_HELPDESK_API_TOKEN",
"extraHeaders": {
"x-organization-id": "org-123"
}
}
}

Headers:

Authorization: Bearer <your-jwt>
x-huat-platform: customer
{
"data": {
"createCustomAction": {
"id": 3,
"name": "Create Support Ticket",
"url": "https://api.example.com/tickets",
"method": "POST"
}
}
}

Example: session-identity-driven action (OmniChat/aisCRM)

Section titled “Example: session-identity-driven action (OmniChat/aisCRM)”

No bearerToken — the credential resolves live, per call, from whoever is signed in to the conversation via WebbyX One (see Credential sources above and sdkVerifyWebbyxOneIdentity):

{
"input": {
"name": "OmniChat (session identity)",
"url": "https://api.omnichat.example/graphql",
"method": "POST",
"credentialSource": "SESSION_OMNICHAT"
}
}

Updates a custom action owned by the caller’s tenant.

Auth: JWT dashboard auth only.

NameTypeRequiredDescription
input.idInt!YesAction to update.
input.nameStringNoNew display name.
input.urlStringNoNew target endpoint. Rejected if the action’s credentialSource is a Wetel-provisioned shared credential (OMNICHAT/NORTIA/AISCRM/LARK_TENANT) — see Credential sources above. Also checked against the same private/internal-address (SSRF) guard as every other tenant-controlled outbound URL, and rejects redirect-only targets at call time.
input.methodStringNoNew HTTP method. Same immutability restriction as input.url for a shared-credential action.
input.descriptionStringNoNew description.
input.argsSchemaJSONNoNew arguments schema.
input.extraHeadersJSONNoReplaces the action’s entire extraHeaders map. Omit the field to leave the existing value unchanged; pass {} (or null) to clear it — this is a full replace, not a per-key merge.
input.credentialSecretHeaderNameStringNoRetarget the action’s stored secret onto a different header name, or pass null to clear back to the default Authorization: Bearer behavior. See “Credential header name” above. Rejected if the action has no stored bearerToken, or if its credentialSource is a Wetel-provisioned/session-identity value.
input.credentialSourceStringNoSwitch this action onto (or between) "SESSION_OMNICHAT"/"SESSION_AISCRM"/"SESSION_NORTIA" after creation. Same restrictions as createCustomAction — see Credential sources above. Switching onto a SESSION_* source clears any stale stored credential the action previously had.

Note bearerToken cannot be changed via updateCustomAction — it is only accepted on create. To rotate the credential, delete and re-create the action (and update any workflow ACTION node that referenced the old action id).

CustomActionDto! — the updated action.

mutation UpdateCustomAction($input: UpdateCustomActionInput!) {
updateCustomAction(input: $input) {
id
name
url
method
}
}
{
"input": {
"id": 3,
"url": "https://api.example.com/v2/tickets",
"description": "Opens a new support ticket in the external helpdesk (v2 endpoint)."
}
}

Headers:

Authorization: Bearer <your-jwt>
x-huat-platform: customer
{
"data": {
"updateCustomAction": {
"id": 3,
"name": "Create Support Ticket",
"url": "https://api.example.com/v2/tickets",
"method": "POST"
}
}
}

Deletes a custom action owned by the caller’s tenant.

Auth: JWT dashboard auth only.

NameTypeRequiredDescription
idInt!YesAction to delete.

Boolean!true on success.

As with deleteMcpConnector, deleting an action still referenced by a published workflow’s ACTION node will break that node at runtime.

mutation DeleteCustomAction($id: Int!) {
deleteCustomAction(id: $id)
}
{ "id": 3 }

Headers:

Authorization: Bearer <your-jwt>
x-huat-platform: customer
{ "data": { "deleteCustomAction": true } }
  • For a quick demo, a custom action is usually faster to stand up than a full MCP connector — if the third-party system only needs one or two REST calls, skip building/hosting an MCP server and use createCustomAction directly.
  • Always run listMcpTools against a connector once, right after creating it and before wiring it into a workflow — confirming the actual tool names/schemas up front avoids a live demo failure caused by a guessed tool name that doesn’t exist.
  • Use a scoped-down, disposable credential (API key or bearer token) for demo connectors/actions rather than a production credential — demo environments get shared, screen-recorded, and sometimes left running longer than intended.
  • Have a fallback in mind for any workflow branch that depends on a demo connector/action — a flaky third-party demo endpoint is a common source of “it worked in rehearsal” failures.
  • Rotate credentials on a schedule: since neither updateMcpConnector nor updateCustomAction can change the stored secret, rotating a production credential means delete-and-recreate — plan for the brief window where any workflow node referencing the old id needs to be repointed to the new one, and do this as a single coordinated change (update the connector/action, then updateWorkflow + publishWorkflow on every affected workflow) rather than leaving a workflow pointed at a deleted id.
  • Before deleting any MCP connector or custom action, check every workflow for a tool/ACTION node referencing it — there’s no built-in reference check, so a stale id in a live workflow only surfaces as a runtime failure, visible via workflowRun’s nodeTrace (see Workflows API) rather than at delete time.
  • Treat third-party MCP servers and REST endpoints backing production actions as external dependencies with their own SLAs — apply the same monitoring/alerting discipline you’d apply to any other production integration, since a workflow node calling out to a down third party fails the whole conversation turn, not just that node.
  • Because these operations are JWT-only (no API-key path), any infrastructure-as-code or CI/CD process that provisions connectors/actions as part of a deployment pipeline needs a JWT-authenticated service identity — factor this into your automation design early rather than discovering the API-key gap mid-rollout.
  • Since apiKey/bearerToken are write-only and never returned by any query, keep your own secure record of which credential is currently active for each connector/action (e.g. in your own secrets manager) — the Wetel API itself gives you no way to retrieve or diff a previously-set credential.

See Workflows API for how connectors and actions are referenced from tool/ACTION nodes, MCP Connectors for the conceptual guide and known limitations, and API Reference Overview for general auth and header conventions.