Skip to content

Guided setup

These functions are used from the guided_setup script of an app, to describe the setup wizard that is shown in the studio after the app has been installed on a bot.

Three functions drive the wizard:

  • setup_init() is called once when the user starts the guided setup. It must return a setup, built with Setup.new/1.
  • setup_step(setup, step) is called every time the user completes a step. It receives the setup as it currently stands and the completed step with the data the user filled in, and must return the setup again. The step is not part of the setup yet: putting it back with Setup.replace_step/2 accepts the change, returning the setup without it rejects the change. Returning a further modified setup is how steps are added, removed, skipped or pre-filled while the wizard is running.
  • setup_finish(setup, per_step_data) is called once after the final step. This step should be used to write any necessary scripts, call any APIs, etc.

Every step is rendered as a header with the title and description, an optional list of best practices, the form described by the step's schema and ui_schema, and a button to continue to the next step. Steps that are disabled are skipped.

A somewhat complete example:

function setup_init() do
  return Setup.new(
    steps: [
      # First ask for some bot-identity stuff.
      step_identity(),
      # Then gather the knowledgebase.
      Setup.knowledgebase_step(),
      # Now that we have the knowledgebase, we can prepare the system prompt.
      step_prompt(),
      # Now setup the channels to communicate over.
      Setup.channels_step(),
      # Review & finalize.
      Setup.review_step(),
    ]
  )
end

function setup_step(_setup, _step) do
  _setup = Setup.replace_step(_setup, _step)
  _setup = prefill_prompt(_setup, _step)
  _setup = prepare_review(_setup, _step)
  return _setup
end

function setup_finish(_setup, _data) do
  # Read the prompts.yaml & update it to include the knowledgebases.
  _kbs = []

  repeat _kb in _data["$knowledgebase"].knowledge_bases do
    _kbs = _kbs + [%{"id" => _kb.id, "label" => _kb.label, _kb.strategy => %{}}]
  end

  _prompts = %{
    prompts: [
      %{
        id: "agent",
        label: "Agent",
        text: _data.prompt.prompt
      }
    ],
    knowledge: _kbs
  }

  # Prepare the prompts script.
  write_script("prompts", yaml_build(_prompts), type: "text/yaml+prompts")
end

# Minimal example of prompt prefilling.
@prompt_templates %{
  "receptionist" => %{
    "opening_text" => "You are speaking to an AI assistant.",
    "prompt" => """
    You are a receptionist for a company named {{ name }}.

    {{ description }}

    You will receive phone calls and answer inquires from the knowledgebase,
    or figure out which department people want to talk to and forward them to
    the right location.

    {{ kbs }}

    [[ full_transcript ]]
    """
  },
  "coworker" => %{
    "prompt" => """
    You are an employee working for {{ name }}.

    {{ description }}

    Answer emails coming in from fellow co-workers.
    """
  }
}

function prefill_prompt(_setup, _step) do
  if _step.id not in ["identity", "$knowledgebase"] do
    return _setup
  end

  # We'll ensure the knowledgebases & company info are included
  # in the prompt step.
  _identity = Setup.get_step_by_id(_setup, "identity")
  _kb = Setup.get_step_by_id(_setup, "$knowledgebase")
  _data = @prompt_templates[_identity.data.type]
  _kbs = []

  repeat _kb in _kb.data.knowledge_bases do
    _kbs = _kbs + ["[! #{_kb.id} ]"]
  end

  if _data do
    _data.prompt = liquid(_data.prompt, %{
      "name" => _identity.data.name,
      "description" => _identity.data.description,
      "kbs" => join(_kbs, "\n")
    })

    # NOTE: We are unconditionally changing the prompt step here.
    # If someone filled in the prompt step then went back to the
    # identity step to correct something, this would override their
    # work in the prompt step, which might not be desirable.
    _setup = Setup.set_data(_setup, "prompt", _data)
  end

  return _setup
end

@type_labels %{
  "receptionist" => "Receptionist",
  "blank" => "Custom",
  "coworker" => "Co-worker",
}

function prepare_review(_setup, _step) do
  branch _step.id do
  "identity" ->
    _step.review = Setup.review(
      title: "Agent Details & Persona",
      icon: "badge",
      value: %{
        name: _step.data.name,
        type: @type_labels[_step.data]
      },
      ui_schema: %{
        name: %{"ui:title" => "Company name"},
        type: %{"ui:title" => "Agent role"}
      }
    )

  "prompt" ->
    _step.review = Setup.review(
      title: "System Prompt & Instructions",
      icon: "document",
      value: _step.data.prompt,
      ui_schema: %{"ui:widget" => "markdown"}
    )

  "$channels" ->
    _step.review = Setup.review(
      title: "Active Communication Channels",
      icon: "phone",
      value: _step.data.channel_names,
      ui_schema: %{"ui:widget" => "channels"}
    )
  end

  return Setup.replace_step(_setup, _step)
end

## The steps

function step_identity() do
  return Setup.step(
    id: "identity",
    name: "Agent identity",
    title: "What is this agent for?",
    description: "Choose the conversational setting. This adjusts optimal prompts, channels and tone configuration templates.",
    card: false,
    schema: %{
      "type" => "object",
      "required" => ["type", "name", "description"],
      "properties" => %{
        "type" => %{
          "type" => "string",
          "oneOf" => [
            %{
              "const" => "blank",
              "title" => "Blank agent",
              "description" => "Start from an empty prompt"
            },
            %{
              "const" => "receptionist",
              "title" => "Receptionist",
              "description" => "Callers · phone-first. Best for handling incoming business lines, call forwarding, and FAQ support."
            },
            %{
              "const" => "coworker",
              "title" => "Coworker",
              "description" => "Employees · Teams/web. Tailored for workspace integrations, text chat channels, and employee support."
            }
          ]
        },
        "name" => %{
          "type" => "string"
        },
        "description" => %{
          "type" => "string",
          "maxLength" => 150
        }
      }
    },
    ui_schema: %{
      "ui:options" => %{
        "field" => "sub_objects",
        "groups" => [
          %{
            "name" => "type",
            "properties" => ["type"],
            "ui:options" => %{
              "label" => false
            }
          },
          %{
            "name" => "company",
            "properties" => ["name", "description"],
            "card" => true,
            "ui:options" => %{
              "title" => "Company"
            }
          }
        ]
      },

      "type" => %{
        "ui:options" => %{
          "label" => false,
          "widget" => "radio_card",
          "value_opts" => %{"blank" => %{"horizontal" => true}}
        }
      },

      "name" => %{
        "ui:options" => %{
          "title" => "Company name"
        }
      },

      "description" => %{
        "ui:options" => %{
          "title" => "Description",
          "help" => "Write about what the company does"
        }
      },

    },
    data: %{"type" => "receptionist", "name" => bot.title}
  )
end

function step_prompt() do
  return Setup.step(
    id: "prompt",
    name: "Prompt",
    title: "Configure agent prompt",
    description: "Define how your agent communicates. Write the system prompt that shapes its personality, tone, and response behavior.",
    best_practices: [
      Setup.best_practice(
        icon: "tick",
        title: "Keep instructions concise and specific",
        description: "Focus on clear rules and limits instead of broad creative briefs."
      ),
      Setup.best_practice(
        icon: "chat",
        title: "Define role and tone upfront",
        description: "Give your agent a clear corporate identity and conversational boundaries."
      ),
      Setup.best_practice(
        icon: "variable",
        title: "Use dynamic personalization variables",
        description: "Inject customer metadata fields using double-bracket notation."
      )
    ],
    schema: %{
      type: "object",
      title: "System Instructions",
      description: "Configure exactly how the AI agent acts, handles intent, and represents your brand during calls.",
      properties: %{
        opening_text: %{
          type: "string",
          title: "Opening text"
        },
        closing_text: %{
          type: "string",
          title: "Closing text"
        },
        prompt: %{
          type: "string",
          title: "System prompt"
        }
      }
    },
    ui_schema: %{
      "opening_text" => %{
        "ui:help": "Use this to tell the end user about the agent to comply with the EU AI Act."
      },
      "prompt" => %{
        "ui:options" => %{
          "widget" => "markdown",
          "prompt_editor" => true,
          "max_length" => 1000,
          "placeholder" => "Press / to insert variables, tools, and more",
          "help" => "Press / to insert variables, tools, and more"
        }
      },
      "ui:order" => [
        "opening_text",
        "closing_text",
        "prompt"
      ]
    }
  )
end

Setup.append_step(setup, step)

Add a step at the end of the wizard.

Setup.best_practice(attrs)

Build a "best practice" callout for a step.

Options:

  • :icon - name of a Blueprint icon.
  • :title - the short heading of the callout.
  • :description - the advice itself.

Setup.channels_step(opts \\ [])

Build the standard step in which the user picks the channels that the bot should be reachable on.

The ID for the step is always "$channels".

The UI elements name, title, description, and best_practices can be overwritten, the rest are provided and managed by the platform.

In the setup_step callback this step's data will contain information about the configured channels in the following format:

%{
  "channel_names" => ["phone", "email"],
}

Setup.delete_step(setup, id)

Remove the step with the given identifier.

Setup.get_step_by_id(setup, id)

Look up a step by its identifier. Returns nil when there is no such step.

Setup.insert_after(setup, id, step)

Insert a step directly after the step with the given identifier.

Setup.insert_before(setup, id, step)

Insert a step directly before the step with the given identifier.

Setup.knowledgebase_step(opts \\ [])

Build the standard step in which the user configures the knowledgebase that the bot uses for the agent.

The ID for the step is always "$knowledgebase".

The UI elements name, title, description, and best_practices can be overwritten, the rest are provided and managed by the platform.

In the setup_step callback this step's data will contain information about the configured knowledge bases in the following format:

%{
  "knowledge_bases" => [
    %{
      "id" => "example",
      "label" => "Example",
      "strategy" => "managed_vertex_rag"
    }
  ],
}

Setup.new(attrs)

Build the setup that describes the entire wizard.

The only option is :steps, a non-empty list of steps as returned by Setup.step/1 or Setup.channels_step/0.

Setup.new(
  steps: [
    Setup.channels_step(),
    Setup.step(
      id: "greeting",
      title: "Greeting",
      description: "How the bot says hello",
      schema: %{"type" => "string"},
      ui_schema: %{}
    )
  ]
)

Setup.replace_step(setup, step)

Replace the step that has the same identifier as the given step.

Raises when the setup has no step with that identifier.

Setup.review(attrs)

Builds a review UI for a step.

If the step doesn't have one it's not rendered in the final review step.

Options:

  • :title - The header for the section in the review step. Defaults to the name of the step.
  • :value - The value to render in the review.
  • :ui_schema - A UI schema similar to one you'd use in a form. Because the review doesn't render a form but just the finished data, it doesn't support the same fields/widgets as a regular UI schema.

Setup.review_step(opts \\ [])

Includes a final review step.

Requires that the relevant review field is set on each step you want visible in this step.

The ID for the step is always "$review".

The UI elements name, title, description, and best_practices can be overwritten, the rest are provided and managed by the platform.

This step does not return any data itself. Instead, accepting the review triggers the setup_finish callback.

Setup.set_data(setup, id, data)

Pre-fill the form of the step with the given identifier.

Setup.set_disabled(setup, id, disabled)

Enable or disable the step with the given identifier. Disabled steps are skipped by the wizard.

Setup.step(attrs)

Build a single step of the wizard.

Options:

  • :id - identifier of the step; this is how setup_step recognises it.
  • :title - the heading shown above the form.
  • :description - explains what the user is asked to do.
  • :schema - the JSON schema of the form.
  • :ui_schema - the accompanying uiSchema, which controls how the form is rendered.
  • :card - render the form inside a card. Defaults to true.
  • :disabled - when true the step is skipped. Defaults to false.
  • :data - data to pre-fill the form with.
  • :best_practices - a list of Setup.best_practice/1 callouts, shown between the header and the form.