---
title: Publish as model
path: concepts/publish-as-model
status: published
---

# Publish as model

A deployed Core can be **registered as a chat model** that any OpenAI-compatible client can reach. This is how a ScaiFlow agent becomes addressable from ScaiBot, custom apps, third-party integrations, or just `curl`.

## The flag

On the flow's Flow properties panel, check **Publish as chat model**:

```jsonc
"config": {
  "publish_as_model": true,
  "model_visibility_group_ids": ["group_acme_eng", "group_acme_ops"]
}
```

The compiler surfaces this on the YAML manifest under `manifest.publish_as_model` and `manifest.model_visibility_group_ids`. The deploy step reads them and acts.

## What happens at deploy

1. ScaiFlow's `DeployService.deploy_flow` first calls `POST /v1/modules/scaicore/cores` to create the Core (returns `core_id`).
2. If `publish_as_model` is `true`, it then calls `POST /v1/modules/scaicore/cores/{core_id}/publish` with optional `group_ids` as `CorePublishRequest.group_ids`.
3. ScaiGrid sets the Core's runtime mode to `model` and registers a `FrontendModel` reachable via `/v1/chat/completions` at slug `scaicore/{tenant_slug}/{core.slug}`.

The publish call is idempotent — re-deploying with `publish_as_model: true` updates the registration; toggling it off doesn't currently un-publish (that requires a manual admin call). See [Troubleshooting: changing publish state](../troubleshooting/deploy-failures).

## Visibility scoping

The optional `model_visibility_group_ids` array forwards to ScaiKey-group scoping on the published `FrontendModel`. Members of any listed group can call the model; others get a 403 from `/v1/chat/completions`. Leave empty for visibility to the default scope (typically the whole tenant).

## Invoking the published model

```bash
TENANT="acme"
SLUG="customer-support"

curl -X POST "https://scaigrid.scailabs.ai/v1/chat/completions" \
  -H "Authorization: Bearer ${SCAIGRID_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "scaicore/'"$TENANT"'/'"$SLUG"'",
    "messages": [
      {"role": "user", "content": "I'\''d like a refund for order 42"}
    ]
  }'
```

The Core's entry node receives the request payload as input (mapping varies by entry kind — for `entry_api`, the payload's `input` field is what reaches the trigger's emitted scope).

## Why this isn't an entry kind

There is no `entry_chat` node. The chat surface is owned by ScaiGrid's `/v1/chat/completions` endpoint, which routes to a Core via its published slug. The Core itself doesn't have a "chat trigger" — its entry is still `api` or `webhook`. The OpenAI-compatible layer is a thin shim ScaiGrid manages.

This split keeps the flow graph honest: there's no special chat-shaped block kind in the IR, no risk of authors trying to mix chat-state-machine semantics into a graph that the runtime would silently ignore.

## Conversational state (entity mode)

Stateless Cores (the default) treat every chat completion request independently. For multi-turn conversations with memory, set the Core's instance mode to `entity` (per-conversation instance) and use the `entity_id` field on invocations to thread state. This is a ScaiCore-level concern; ScaiFlow doesn't currently expose `instance_mode` on the canvas — set it via `manifest.scaiflow_meta` or compile and deploy manually until the canvas picks it up. See [ScaiCore: Instance modes](/docs/scaicore).
