Creating and publishing a flow
Guided selling is not yet available. This page describes it ahead of its release, so details can still change. The changelog announces when it is released.
A flow goes live in three steps: you create it with version 1 as a draft, you test the draft and you publish it. Every later change to the content is a new version that you test and publish the same way. The operations on this page need the Configuration role, see Access.
Creating a flow
guidedSellingFlowCreate creates the flow with version 1 as a draft:
mutation CreateFlow($input: GuidedSellingFlowCreateInput!) {
guidedSellingFlowCreate(input: $input) {
id
name
active
version
}
}
The input holds a name for the flow, the Anthropic API key its model calls are billed to and the content of version 1. The complete variables for a label printer advisor with four printers, a print and apply system as a configurable cluster and ribbons to offer as an add-on:
Variables
{
"input": {
"name": "Label printer advisor",
"modelApiKey": "YOUR_ANTHROPIC_API_KEY",
"content": {
"instruction": "# Label printer advisor\n\nYou help warehouse managers, operations leads and office staff choose a label printer from this catalog.\n\n## How to narrow down\n\n1. Where the printer is used: a packing desk or office; a warehouse or production floor; on the move; or on a conveyor line, applying labels automatically.\n2. How many labels the customer prints on a busy day.\n3. The widest label they print: 2, 4 or 6 inch.\n4. How the printer connects: USB, Ethernet, Wi-Fi or Bluetooth.\n\nRule printers out on what the customer tells you. Do not ask a question whose answer would not change the advice.\n\n## Rules\n\n- On the move means battery power and a wireless connection: only mobile printers fit.\n- More than 5,000 labels a day needs an industrial printer.\n- Labels wider than 4 inch need a 6-inch printer.\n- Labels applied automatically on a conveyor need the LP-A800 print and apply system. Ask on which side of the conveyor it stands and advise that variant. Boxes of different heights need the air blow applicator. Add the missing label sensor when the customer must not ship a box without a label, and the service plan only when they ask for on-site service.\n- Thermal transfer labels last longer than direct thermal ones. Mention it when labels are stored outdoors or for months.\n- When the customer already knows which model they want, ask which one and advise it.\n\n## With the printer\n\nOffer one box of thermal transfer ribbons with every industrial printer and print and apply system when labels are stored outdoors or for months.\n\n## In the advice\n\nIn each reason, name the customer's needs that the printer meets. When no printer here meets a need, say so and suggest talking to a specialist.",
"catalogue": {
"products": [
{
"productId": 18240,
"sku": "LP-D420",
"name": "LP-D420 Desktop Label Printer",
"description": "Compact printer for shipping labels at a packing desk or in an office.",
"attributes": [
{ "name": "ENVIRONMENT", "value": "Office, packing desk" },
{ "name": "LABELS_PER_DAY", "value": "Up to 500" },
{ "name": "MAX_LABEL_WIDTH", "value": "4 inch" },
{ "name": "PRINT_METHOD", "value": "Direct thermal" },
{ "name": "CONNECTIVITY", "value": "USB, Ethernet" }
]
},
{
"productId": 18243,
"sku": "LP-I440",
"name": "LP-I440 Industrial Label Printer",
"description": "Metal-cased printer for continuous labeling in a warehouse.",
"attributes": [
{ "name": "ENVIRONMENT", "value": "Warehouse, production floor" },
{ "name": "LABELS_PER_DAY", "value": "Up to 5,000" },
{ "name": "MAX_LABEL_WIDTH", "value": "4 inch" },
{ "name": "PRINT_METHOD", "value": "Thermal transfer, direct thermal" },
{ "name": "CONNECTIVITY", "value": "USB, Ethernet, Wi-Fi" }
]
},
{
"productId": 18241,
"sku": "LP-I640",
"name": "LP-I640 Industrial Label Printer",
"description": "High-volume printer for pallet and shipping labels up to 6 inch wide.",
"attributes": [
{ "name": "ENVIRONMENT", "value": "Warehouse, production floor" },
{ "name": "LABELS_PER_DAY", "value": "Up to 10,000" },
{ "name": "MAX_LABEL_WIDTH", "value": "6 inch" },
{ "name": "PRINT_METHOD", "value": "Thermal transfer, direct thermal" },
{ "name": "CONNECTIVITY", "value": "USB, Ethernet, Wi-Fi" }
]
},
{
"productId": 18242,
"sku": "LP-M220",
"name": "LP-M220 Mobile Label Printer",
"description": "Battery-powered printer worn on the belt for shelf and picking labels.",
"attributes": [
{ "name": "ENVIRONMENT", "value": "Warehouse aisles, customer sites" },
{ "name": "LABELS_PER_DAY", "value": "Up to 300" },
{ "name": "MAX_LABEL_WIDTH", "value": "2 inch" },
{ "name": "PRINT_METHOD", "value": "Direct thermal" },
{ "name": "CONNECTIVITY", "value": "Bluetooth, Wi-Fi" },
{ "name": "POWER_SOURCE", "value": "Battery" }
]
},
{
"productId": 18250,
"sku": "TTR-110-450",
"name": "Thermal transfer ribbon 110 mm x 450 m, box of 12",
"description": "Wax-resin ribbons for long-lasting labels on industrial printers and print and apply systems."
}
],
"clusters": [
{
"clusterId": 1830,
"sku": "LP-A800",
"name": "LP-A800 Print and Apply System",
"description": "Prints labels and applies them to boxes on a conveyor line, without an operator.",
"attributes": [
{ "name": "ENVIRONMENT", "value": "Conveyor line" },
{ "name": "LABELS_PER_DAY", "value": "Up to 20,000" },
{ "name": "MAX_LABEL_WIDTH", "value": "4 inch" },
{ "name": "PRINT_METHOD", "value": "Thermal transfer" }
],
"variants": [
{
"productId": 18301,
"sku": "LP-A800-L",
"name": "LP-A800 Print and Apply System, left-hand",
"attributes": [{"name": "CONVEYOR_SIDE", "value": "Left"}]
},
{
"productId": 18302,
"sku": "LP-A800-R",
"name": "LP-A800 Print and Apply System, right-hand",
"attributes": [{"name": "CONVEYOR_SIDE", "value": "Right"}]
}
],
"defaultVariantId": 18302,
"options": [
{
"clusterOptionId": 1831,
"name": "Applicator",
"isRequired": true,
"defaultProductId": 18311,
"products": [
{
"productId": 18311,
"sku": "LP-A800-TAMP",
"name": "Tamp applicator",
"description": "Presses the label onto the box. For boxes of one height."
},
{
"productId": 18312,
"sku": "LP-A800-BLOW",
"name": "Air blow applicator",
"description": "Blows the label onto the box. For boxes of different heights."
}
]
},
{
"clusterOptionId": 1832,
"name": "Label sensor",
"isRequired": false,
"products": [
{
"productId": 18321,
"sku": "LP-A800-SENS",
"name": "Missing label sensor",
"description": "Stops the line when a box leaves without a label. One per conveyor lane.",
"defaultQuantity": 1,
"maxQuantity": 2
}
]
},
{
"clusterOptionId": 1833,
"name": "Service plan",
"isRequired": false,
"products": [
{
"productId": 18331,
"sku": "SVC-ONSITE-3Y",
"name": "3-year on-site service",
"description": "A technician on site within one business day, for three years."
}
]
}
]
}
]
},
"settings": {
"language": "EN",
"languages": ["EN", "NL"],
"opener": {
"questions": [
{ "language": "EN", "value": "Where will the printer be used?" },
{ "language": "NL", "value": "Waar gaat u de printer gebruiken?" }
],
"options": [
{
"labels": [
{ "language": "EN", "value": "At a packing desk or in an office" },
{ "language": "NL", "value": "Aan een inpaktafel of op kantoor" }
]
},
{
"labels": [
{ "language": "EN", "value": "In a warehouse or on the production floor" },
{ "language": "NL", "value": "In een magazijn of in de productie" }
]
},
{
"labels": [
{ "language": "EN", "value": "On the move: in the aisles or at customer sites" },
{ "language": "NL", "value": "Onderweg: tussen de stellingen of bij klanten" }
]
},
{
"labels": [
{ "language": "EN", "value": "On a conveyor line, applying labels automatically" },
{ "language": "NL", "value": "Aan een transportband, automatisch etiketteren" }
]
},
{
"labels": [
{ "language": "EN", "value": "I already know which model I want" },
{ "language": "NL", "value": "Ik weet al welk model ik wil" }
]
}
]
},
"freeText": true,
"maxQuestions": 5,
"model": { "model": "claude-sonnet-5-5", "effort": "MEDIUM" }
}
}
}
}
Expected response:
{
"data": {
"guidedSellingFlowCreate": {
"id": "01a10f3c-5b7e-7d42-9a61-3c8e2f4b7d90",
"name": "Label printer advisor",
"active": true,
"version": null
}
}
}
version is the published version, so it stays null until you publish one. The id is what a storefront needs to run the flow.
Content that breaks a rule is refused with BAD_REQUEST or GUIDED_SELLING_CONTENT_INVALID_ERROR, listing every problem at once. See Writing flow content.
Before the flow is stored, guided selling asks Anthropic whether it accepts the key and knows the model of version 1. That check generates nothing. When Anthropic refuses, the flow is not created and the error gives Anthropic's reason:
{
"errors": [
{
"message": "The model vendor refused the guide's key: invalid x-api-key",
"path": ["guidedSellingFlowCreate"],
"extensions": {
"code": "GUIDED_SELLING_MODEL_UNAVAILABLE_ERROR",
"reason": "LLM_AUTH_FAILED",
"retryable": false
}
}
],
"data": null
}
extensions.reason is LLM_AUTH_FAILED for a refused key and LLM_MODEL_NOT_FOUND for a model Anthropic does not know. When Anthropic cannot answer right then, the flow is created anyway.
Testing a draft
Run the draft with guidedSellingStep and its version number, using a key or user with the Configuration role Editor or Owner. Without version, the step runs the published version:
mutation GuidedSellingStep($input: GuidedSellingStepInput!) {
guidedSellingStep(input: $input) {
__typename
... on GuidedSellingQuestion {
questionId
question
acknowledgement
options {
optionId
label
}
freeTextAllowed
}
... on GuidedSellingAdvice {
headline
items {
productId
reason
clusterId
options {
clusterOptionId
productId
quantity
isRequired
}
addOns {
productId
quantity
reason
}
}
}
}
}
Variables:
{
"input": {
"flowId": "01a10f3c-5b7e-7d42-9a61-3c8e2f4b7d90",
"version": 1,
"language": "EN",
"conversationId": "draft-test-1",
"answers": []
}
}
The response is the opening question of version 1. Add answers as a storefront would, see Running a guided selling conversation.
A storefront cannot reach a draft, and neither can a Viewer. Until a version is published, a storefront's steps fail with GUIDED_SELLING_FLOW_NOT_PUBLISHED_ERROR, and a step that names any version other than the published one fails with GUIDED_SELLING_FLOW_VERSION_NOT_ALLOWED_ERROR.
Play the draft through the way different customers would: pick options, type answers, answer "not sure" and ask for something the catalog does not have. Check that each question is relevant, that the advice is right, that a configured cluster has the cluster options it needs, that the add-ons fit and that each reason is accurate. Test conversations are recorded like any other, so you can read them back with guidedSellingConversation.
Publishing a version
mutation PublishVersion($flowId: ID!, $version: Int!) {
guidedSellingFlowVersionPublish(flowId: $flowId, version: $version) {
version
status
publishedAt
warmStatus
}
}
Variables:
{
"flowId": "01a10f3c-5b7e-7d42-9a61-3c8e2f4b7d90",
"version": 1
}
Expected response:
{
"data": {
"guidedSellingFlowVersionPublish": {
"version": 1,
"status": "PUBLISHED",
"publishedAt": "2026-10-06T13:02:41.000Z",
"warmStatus": "RUNNING"
}
}
}
Before the switch, guided selling asks Anthropic whether it accepts the flow's key and knows the version's model, the same check as when you create a flow. A refusal stops the publish with GUIDED_SELLING_MODEL_UNAVAILABLE_ERROR and nothing changes for storefronts.
New conversations start on this version right away. The version that was published before is archived, and conversations that started on it can finish on it for a day. After that, their next step returns GUIDED_SELLING_ANSWER_UNKNOWN_ERROR and the storefront starts a new conversation, on the new version. Only a draft can be published: publishing another version fails with GUIDED_SELLING_FLOW_VERSION_NOT_DRAFT_ERROR.
Following the warm-up
Right after publishing, guided selling prepares the first steps of the version: the step after each option of the opening question and the steps one level below, in every language the version offers. Each step is a model call, so an opening question with four options in three languages takes about 60 calls. Guided selling works through them in the background, so your publish does not wait for it, and it picks up where it left off after an interruption. A step that has to wait for the budget of model calls waits its turn instead of failing. warmStatus reports how it went:
query FlowVersions($flowId: ID!, $input: GuidedSellingFlowVersionSearchInput) {
guidedSellingFlowVersions(flowId: $flowId, input: $input) {
items {
version
status
warmStatus
warmError
warmedAt
}
}
}
Variables:
{
"flowId": "01a10f3c-5b7e-7d42-9a61-3c8e2f4b7d90"
}
Expected response:
{
"data": {
"guidedSellingFlowVersions": {
"items": [
{
"version": 1,
"status": "PUBLISHED",
"warmStatus": "DONE",
"warmError": null,
"warmedAt": "2026-10-06T13:04:12.000Z"
}
]
}
}
}
warmStatus | Meaning |
|---|---|
RUNNING | The first steps are being prepared. While the daily cap of model calls is used up, preparing waits, which can take hours. |
DONE | Every step was prepared |
PARTIAL | Some steps could not be prepared. warmError says why. |
FAILED | Nothing could be prepared. warmError has the reason, in Anthropic's words when it came from Anthropic. Customers will likely get the same error. |
The warm-up uses the flow's key and model for real, so it can show problems the check at publishing could not see, for example a key that was revoked later or an account without credit. A refused key (LLM_AUTH_FAILED) is fixed by replacing the key: when the published version's warm-up failed or was partial, replacing the key runs it again. A model Anthropic does not know (LLM_MODEL_NOT_FOUND) needs a new version with another model.
Changing the content
Read the content of the current version, change it and send it back as a new version. Versions are listed newest first, and content has the same shape as the input:
query FlowContent($flowId: ID!, $input: GuidedSellingFlowVersionSearchInput) {
guidedSellingFlowVersions(flowId: $flowId, input: $input) {
items {
version
status
content {
instruction
catalogue {
products {
productId
sku
name
description
attributes {
name
value
}
}
clusters {
clusterId
sku
name
description
attributes {
name
value
}
variants {
productId
sku
name
description
attributes {
name
value
}
}
defaultVariantId
options {
clusterOptionId
name
description
isRequired
defaultProductId
products {
productId
sku
name
description
attributes {
name
value
}
defaultQuantity
maxQuantity
}
}
}
}
settings {
language
languages
freeText
maxQuestions
opener {
questions {
language
value
}
options {
labels {
language
value
}
}
}
model {
model
effort
}
}
}
}
}
}
Variables:
{
"flowId": "01a10f3c-5b7e-7d42-9a61-3c8e2f4b7d90",
"input": { "offset": 1 }
}
With offset set to 1, the response holds only the newest version. Create the next version from its content with guidedSellingFlowVersionCreate:
mutation CreateVersion($flowId: ID!, $input: GuidedSellingFlowVersionCreateInput!) {
guidedSellingFlowVersionCreate(flowId: $flowId, input: $input) {
version
status
}
}
A script that reads the newest version, lowers its question budget and saves the result as a new draft:
const FLOW_CONTENT = `...`; // the FlowContent query above
const CREATE_VERSION = `...`; // the CreateVersion mutation above
const flowId = '01a10f3c-5b7e-7d42-9a61-3c8e2f4b7d90';
async function propeller(query: string, variables: object) {
const response = await fetch('https://api.helice.cloud/v2/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json', apiKey: process.env.PROPELLER_API_KEY! },
body: JSON.stringify({ query, variables }),
});
const { data, errors } = await response.json();
if (errors?.length) throw new Error(errors[0].message);
return data;
}
const read = await propeller(FLOW_CONTENT, { flowId, input: { offset: 1 } });
const { content } = read.guidedSellingFlowVersions.items[0];
content.settings.maxQuestions = 4;
const created = await propeller(CREATE_VERSION, { flowId, input: { content } });
console.log(created.guidedSellingFlowVersionCreate);
The script prints the new draft:
{
"version": 2,
"status": "DRAFT"
}
Test version 2 by naming it in the step, then publish it. Storefronts keep running version 1 until you do.
Renaming, switching off and replacing the key
guidedSellingFlowUpdate changes only the fields you send:
mutation UpdateFlow($id: ID!, $input: GuidedSellingFlowUpdateInput!) {
guidedSellingFlowUpdate(id: $id, input: $input) {
id
name
active
lastModifiedAt
}
}
Variables:
{
"id": "01a10f3c-5b7e-7d42-9a61-3c8e2f4b7d90",
"input": {
"active": false
}
}
Expected response:
{
"data": {
"guidedSellingFlowUpdate": {
"id": "01a10f3c-5b7e-7d42-9a61-3c8e2f4b7d90",
"name": "Label printer advisor",
"active": false,
"lastModifiedAt": "2026-10-06T14:10:05.000Z"
}
}
}
| Field | Effect |
|---|---|
name | Changes the display name. Customers never see it. Editor or Owner. |
active | false switches the flow off: storefronts get GUIDED_SELLING_FLOW_INACTIVE_ERROR until you switch it on again. Editors and Owners can still test its versions, but nothing is warmed while it is off. Owner only. |
modelApiKey | Replaces the model key. It takes effect on the next step, without a new version. Owner only. |
A new key gets the same check as when you create a flow, with the model of the published version, or of the newest version while none is published. A refused key is not saved: the update returns GUIDED_SELLING_MODEL_UNAVAILABLE_ERROR and the flow keeps its old key. When the published version's warm-up failed or was partial, saving a new key runs the warm-up again. Switching a flow on runs the warm-up of its published version when it never ran, for example because the version was published while the flow was off.
Listing flows
query Flows($input: GuidedSellingFlowSearchInput) {
guidedSellingFlows(input: $input) {
itemsFound
items {
id
name
active
version
lastModifiedAt
}
}
}
Variables:
{
"input": {
"active": true,
"sortInputs": [{ "field": "NAME", "order": "ASC" }]
}
}
Expected response:
{
"data": {
"guidedSellingFlows": {
"itemsFound": 1,
"items": [
{
"id": "01a10f3c-5b7e-7d42-9a61-3c8e2f4b7d90",
"name": "Label printer advisor",
"active": true,
"version": 1,
"lastModifiedAt": "2026-10-06T13:02:41.000Z"
}
]
}
}
}
Without sortInputs, flows are listed most recently changed first. Filter with ids, active, createdAt and lastModifiedAt, sort on NAME, CREATED_AT or LAST_MODIFIED_AT, and page through the results with page and offset.
Deleting a flow
mutation DeleteFlow($id: ID!) {
guidedSellingFlowDelete(id: $id)
}
Deleting a flow removes it with every version, stored step and recorded conversation. The mutation returns true, and the deletion cannot be undone. Storefronts that still use the flow id get GUIDED_SELLING_FLOW_NOT_FOUND_ERROR, so remove guided selling from the storefront first, or switch the flow off when you want to keep its history.