# Istari Digital — Tutorials


---

<!-- Source: administer/configure-partner-sharing.md -->

---
sidebar_position: 2
sidebar_label: Configure Secure Connections
title: Configure partner sharing (Secure Connections)
---

# Configure partner sharing (Secure Connections)

Secure Connections let you share individual **models** and **artifacts** with a partner organization through authenticated, encrypted channels. An outbound **Sending Connection** pushes Resources you own to a partner; an inbound **Receiving Connection** accepts Resources a partner pushes to you. **Systems** cannot be shared through a Secure Connection as a whole.

This tutorial is for **administrators** configuring partner sharing in the Istari Digital web app. You set up **Sending** and **Receiving Connections**, hand off credentials to a partner admin, and verify that a shared model syncs. IT must enable partner sharing on your deployment first — you confirm it is available, then complete every connection step in the browser.

By the end you will:

- Create a **Sending Connection**, then set **tag rules** when control tags decide which Resources may pass
- Exchange **Shared ID** and **Shared Secret** credentials with the partner admin
- Optionally create a **Receiving Connection** if your org also accepts inbound partner data
- Confirm sync health from **Sync Status** and verify end-user sharing works

**Time:** ~30 minutes (partner coordination and first sync may add wait time).

## Prerequisites

- **Tenant Administrator** or **Platform Administrator** access (on installations without tenant management, organization administrator access). With it, avatar > **Settings** shows an **Admin** group. See the [Administrator Guide](../../users/admin-guide/index.md#prerequisites).
- **Partner sharing enabled on your deployment** — **Secure Connections** must appear under **Admin** in Settings before you start. IT handles installation and storage setup on the cluster; your job is to confirm the feature is ready in the web app:
  1. Open **Settings** > **Admin** > **Secure Connections**. The page loads with **Sending Connections** and **Receiving Connections** tabs.
  2. Open **Add Secure Connection** and check the dialog for your role:
     - **Sending org** — **Add Sending Connection** lists at least one **Outbox** in the dropdown. An outbox is the staging location for Resources leaving your registry toward a partner. Each sending connection uses one outbox.
     - **Receiving org** — **Add Receiving Connection** lists at least one **Inbox** in the dropdown. An inbox is where inbound partner Resources land before they appear in your registry. Each receiving connection uses one inbox.

  Ask IT which outbox or inbox name to select for your partner — names differ by environment. If **Secure Connections** is missing or the dropdown is empty, hand off to IT using [Secure Connection Service Setup](../../icbs-install/09-secure-connection-service.md) and wait before continuing.

- **A partner registry** — another Istari Digital deployment whose admin can create the matching connection on their side. Agree in advance which org is **sending** and which is **receiving** for this exercise.
- **A tutorial user with Editor access** on a test model — pick a colleague (or a second account you control) who has **Editor** or above on the model. If you completed [Onboard your tenant](onboard-your-organization.md), reuse that tutorial user.
- **A test model** — any small model on **Resources** that the tutorial user can share. Keep it under the ~2 GB per-file transfer limit. The UAS spreadsheet from Web app 101 works well.

---

## Step 1 — Open Secure Connections

1. Sign in to the Istari Digital web app as an administrator.
2. Click your avatar at the bottom of the sidebar, then **Settings**.
3. Under **Admin** in the Settings navigation, select **Secure Connections** (or go to `/admin/secure-connections`).

The page opens on **Sending Connections** by default. Two tabs divide outbound and inbound configuration:

- **Sending Connections** — connections that push your Resources to partners.
- **Receiving Connections** — connections that accept Resources from partners.

Use the **Active / All** toggle (top-right) to include archived connections. For this tutorial, keep **Active** selected.

See [Secure Connections](../../users/admin-guide/secure-connections.md) in the Administrator Guide for column definitions and reference detail.

---

## Step 2 — Agree roles with the partner admin

Partner sharing requires a matched pair of connections — one **sending**, one **receiving** — that share the same **Shared ID** and **Shared Secret**.

| Your org's role                            | You create           | Partner admin creates | Credentials flow                                            |
| ------------------------------------------ | -------------------- | --------------------- | ----------------------------------------------------------- |
| **Sending** (this tutorial's primary path) | Sending Connection   | Receiving Connection  | You generate ID + secret; partner enters them on their side |
| **Receiving**                              | Receiving Connection | Sending Connection    | Partner generates ID + secret; you enter them on your side  |

**The sending side goes first.** The sending admin creates the connection and copies the final **Shared ID** (including any auto-appended suffix) and **Shared Secret** to the receiving admin through your company's approved secure channel.

Confirm each registry's outbox or inbox dropdown is populated (see **Prerequisites**) before either admin creates a connection in Steps 3 and 4.

---

## Step 3 — Create a Sending Connection

Complete this step on the registry that **sends** data to the partner.

1. On **Sending Connections**, click **Add Secure Connection** → **Add Sending Connection**.
2. Fill in **Name** (for example, `Partner — Acme Corp`) and optionally **Description**.
3. Select an **Outbox** from the dropdown (see **Prerequisites**). If that outbox is already bound to another sending connection, a warning appears; pick a different outbox or ask IT to add another.
4. Enter a **Shared ID** base (letters, digits, `.`, `-`, `_` and `/` only — no spaces; see [Nested Shared IDs](../../users/admin-guide/secure-connections.md#nested-shared-ids) for how `/` is used). With **Append unique suffix to Shared ID** checked (default), the platform appends a random suffix on submit so the final ID is globally unique.
5. Enter and confirm a **Shared Secret**. Treat it like a password — it is never retrievable after you close the credentials screen.
6. If infosec is enabled on your deployment, set **Permitted Infosec Level** — the highest infosec level a Resource may carry to be shared over this connection. The field is required whenever it is shown; **Create** stays disabled until you choose a level.
7. Check the certification box confirming this connection will share data with an external organization.
8. Click **Create**.

A credentials screen shows the final **Shared ID** and a masked **Shared Secret**. Copy both before closing — **the secret is shown only once**.

Send the credentials to the partner admin securely. They need the exact **Shared ID** string (suffix included) and the **Shared Secret** to create their receiving connection.

Tag rules are set after the connection exists. Open the connection, go to the **Rules** tab, and add checks that **Allow** or **Block** each Resource based on the control tags it carries. A Resource passes when its tags satisfy those rules. One matching **Block** keeps it from transferring. See [Tag Rules](../../users/admin-guide/secure-connections.md#tag-rules-sending-connections).

---

## Step 4 — Partner creates the Receiving Connection

The partner admin completes this step on **their** registry. Include these instructions in your handoff.

1. **Admin** → **Secure Connections** → **Receiving Connections** → **Add Secure Connection** → **Add Receiving Connection**.
2. Fill in **Name** and optionally **Description**.
3. Select an **Inbox** from the dropdown (see **Prerequisites**).
4. Paste the **Shared ID** and **Shared Secret** from your sending connection (Step 3).
5. Click **Create**.

The creating user becomes the first **Receiving Connection Permitted Recipient** and can see synced Resources that arrive through this connection.

If your org is **both** sides (for example, a paired dev/stage test), switch to the receiving registry and run the steps above yourself using the credentials you generated in Step 3.

---

## Step 5 — Configure receiving-side access (receiving admins)

Skip this section if your org is only the sending side. Receiving admins complete it after Step 4.

### Assign permitted recipients

Permitted recipients of a receiving connection receive **view** access to every Resource that syncs through it.

1. On **Receiving Connections**, open the row **⋯** menu → **Edit permitted recipients** (or click the **Permitted recipients** cell).
2. Search for users who should see inbound partner Resources and add them.
3. Click **Update**.

Permitted recipients still need matching **control tags** and **infosec levels** on their user accounts to see classified or tagged Resources. Assign those tags on **Admin** → **Users**; the connection role covers the share, and the tags cover need-to-know. See [Permitted Recipients](../../users/admin-guide/secure-connections.md#permitted-recipients-receiving-connections).

### Rewrite incoming control tags (receiving admins)

When a partner's Resource arrives with control tags, transformation rules on your receiving connection rewrite those tags onto tags your organization defines. The tags left on the received Resource are need-to-know requirements. A permitted recipient sees the Resource when their account holds every one of those tags.

1. On **Receiving Connections**, open the connection (**Edit connection**) and go to the **Rules** tab.
2. Add a rule for each incoming tag. Each rule needs a **Description** of why it exists.
   - **Replace** — **Predicate** is the incoming tag; **Value** is the local tag the received Resource should require.
   - **Remove** — **Predicate** is an incoming tag you do not want to keep.
   - **Add** — **Value** is a local tag to stamp on every received Resource. Use this when every arrival should require that tag.
3. Create any missing local tag first on **Admin** → **Control Tags**.
4. Click **Save changes**.

An incoming tag that no **Remove** or **Replace** rule covers is skipped, and that Resource does not sync until a rule covers it. After a rule writes a local tag onto a received Resource, assign that tag to the users who should see it, including permitted recipients. See [Transformations](../../users/admin-guide/secure-connections.md#transformations-receiving-connections).

---

## Step 6 — Share a model with the partner (verification)

Confirm the end-user path works. Sign in as your **tutorial user** (Editor or above on the test model), not your administrator account.

1. Open the test model on **Resources**.
2. Click **Share** (share icon) in the resource's action bar.
3. The **Share \<filename\>** dialog opens in user-sharing mode by default. Click **Share with external connection** (globe icon, top-right of the dialog).
4. In **Add organization**, search for and click the sending connection you created in Step 3.
5. The connection appears under **Connections with access (N)**. Review the amber certification box and check **Confirm External Organization(s) will gain access to this model**.
6. Click **Update**. A toast confirms **External connection sharing updated.**

The model's details pane now lists the partner under **Connections with Access**.

On the **receiving** registry, wait one to two minutes, then confirm:

- The model appears for a **Receiving Connection Permitted Recipient** who holds any required control tags and infosec level.
- On **Receiving Connections**, the connection's sync status icon shows success (green check).

If the model does not appear, open **Sync Status** (next step) or see [Debugging Secure Connections](../../users/admin-guide/secure-connections-debugging.md).

**Optional follow-up:** Upload a new revision of the shared model as the tutorial user. The platform pushes the revision to the partner automatically and shows a certification checkbox in the **Upload Version** dialog — see [Actions that automatically push data to partners](../../users/user-guide/secure-connections.md#actions-that-automatically-push-data-to-partners).

---

## Step 7 — Monitor sync status

1. Return to **Settings** > **Admin** > **Secure Connections** as an administrator.
2. On the relevant tab (**Sending** or **Receiving**), click the sync status icon on the connection row.

The **Sync Status** page lists recent sync events, newest first. Each row shows severity, timestamp, and a message preview.

- Click a row (or its chevron) to expand per-resource details. Copy a resource ID from the expanded view when you need it for support.
- Use **With updates** to filter to sync events that changed Resources.
- The page auto-refreshes about every minute while open.

| Icon           | Meaning                                 |
| -------------- | --------------------------------------- |
| Dashed         | No sync activity yet                    |
| Green check    | Last sync succeeded                     |
| Yellow warning | Last sync completed with partial errors |
| Red X          | Last sync failed                        |

Common first-sync failures:

- **Secret mismatch** — receiving **Shared Secret** does not match the sending side. On the receiving connection, use **Reset Secret** (edit dialog) to align with the sender, then wait for the next sync cycle.
- **Uncovered control tags** — add a **Remove** or **Replace** rule for each incoming tag on the receiving connection's **Rules** tab (Step 5).
- **Infosec or tag rules** — the model's classification is above the sending connection's Permitted Infosec Level, or its control tags make the tag rules return **Block**. Adjust the connection or the model's tags and level. See [Cases where a resource is automatically unshared](../../users/user-guide/secure-connections.md#cases-where-a-resource-is-automatically-unshared).

---

## What you set up

| Piece                                           | Why it matters                                                                                                                                                         |
| ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Sending Connection**                          | Lets Editors share models and artifacts with a named partner org through **Share with external connection**.                                                           |
| **Credential handoff**                          | Shared ID + Shared Secret pair the two registries; mismatched secrets block all sync.                                                                                  |
| **Receiving Connection + permitted recipients** | Delivers inbound partner Resources to the right users on your registry.                                                                                                |
| **Tag rules and transformation rules**          | Sending rules allow or block each Resource by its control tags. Receiving rules write your local tags onto what arrives, and those tags decide which users can see it. |
| **Verified share + sync status**                | Proves the full path works before engineers follow the [Secure Connections user guide](../../users/user-guide/secure-connections.md).                                  |

## What's next

- **[Secure Connections (User Guide)](../../users/user-guide/secure-connections.md)** — How Editors share models and artifacts, handle certification prompts, and understand automatic unshare rules.
- **[Secure Connections (Administrator Guide)](../../users/admin-guide/secure-connections.md)** — Reference for connection settings, tag rules, transformation rules, and archiving.
- **Establish classification and control tags** — Planned administrator tutorial for infosec levels and control tags when your program requires them before partner exchange.
- **[Debugging Secure Connections](../../users/admin-guide/secure-connections-debugging.md)** — Resolve sync failures and authentication errors (escalate infrastructure issues to IT).


---

<!-- Source: administer/onboard-your-organization.md -->

---
sidebar_position: 1
title: Onboard your tenant
---

# Onboard your tenant

People cannot run jobs on the Istari Digital Platform until they belong to a tenant and have been granted **tool access**, and until the tenant has a **connected agent** with the integrations they need. User tutorials such as [Web app 101](../connect-build-share/platform-101.md) assume that work is done.

In this tutorial you set up a tenant for a pilot team, then confirm that a member without an administrator role can run `@istari:extract` on a spreadsheet.

By the end you will have:

- A tenant with members, added one at a time and from a CSV file
- A Tenant Administrator for the tenant
- An agent connected to the tenant
- **Tool access** granted for the integration the tutorials use
- A verified extraction job run by a member without an administrator role

**Time:** about 30 minutes, plus the time IT needs to add a new tenant's people and to start the agent.

This tutorial describes installations with [tenant management](../../intro/terminology.md#tenant-management). If Settings has a **Password** entry, your installation does not use tenant management; follow [On installations without tenant management](#on-installations-without-tenant-management) instead.

## Prerequisites

- **An administrator role.** To create a tenant you need the **Platform Administrator** role. A Tenant Administrator onboards their existing tenant instead. See [Administrators](../../users/admin-guide/administrators.md) for what each role can do. If nobody on the installation holds an administrator role yet, your IT administrator [creates the first Platform Administrator](../../icbs-install/03-control-plane/10-identity-service/05-Create-the-First-Platform-Administrator.md).
- **IT help.** For a tenant you create in [Step 2](#step-2--create-the-tenant), your IT administrator adds its people. IT also installs the agent on a host with the **Open Spreadsheet** module, using the key generated in [Step 7](#step-7--connect-an-agent-to-the-tenant).
- **An account for each member.** Each member signs in with an account in your [sign-in provider](../../intro/terminology.md#sign-in-provider). Its verified email address must match the one they are added with. Adding someone in Istari does not create this account. If someone has none, ask your IT administrator to create one; see [Creating New Users](../../icbs-install/10-Zitadel-User-Management/02-creating-new-users.md). On Microsoft Entra ID, IT also needs each member's Entra **Object ID**, because Entra does not match people by email.
- **Your pilot team:**
  - A **tutorial user**: a colleague who will hold no administrator role. You verify job execution as this person in the last step.
  - A **pilot lead** to become the tenant's Tenant Administrator. If you are a Tenant Administrator onboarding your own tenant, this can be you. A Platform Administrator who already belongs to a tenant cannot be the pilot lead of a new one, because a person belongs to at most one tenant.
  - A CSV file listing the rest of the team, with an `email` column and optionally `first_name` and `last_name`. See [File format](../../users/admin-guide/user-management.md#file-format).

Download the sample spreadsheet used in Web app 101; your tutorial user needs it in the last step:

- <a href="/files/microsoft_office_excel/Group3-UAS-Requirements.xlsx" download target="_blank" rel="noopener noreferrer">Group3-UAS-Requirements.xlsx</a>

---

## Step 1 — Confirm your administrator role

1. Sign in to the Istari Digital web app.
2. Click your avatar at the bottom of the sidebar, then **Settings**, and check that the Settings navigation has an **Admin** group. It appears only for administrators.
3. If you are a Platform Administrator, confirm that the avatar menu also offers **Platform Admin Console**.

If the **Admin** group is missing, ask an administrator to grant you a role. If nobody holds one yet, see the [prerequisites](#prerequisites).

---

## Step 2 — Create the tenant

Platform Administrators only. A Tenant Administrator onboards their existing tenant and continues at Step 3.

1. Click your avatar, then **Platform Admin Console**. The **Tenants** page opens.
2. If your team already has a tenant, use it instead of creating one and skip to Step 3. If it is on the **Suspended** tab, [reactivate it](../../users/admin-guide/tenants.md#reactivate-a-tenant) first.
3. Click **New tenant**.
4. Enter a **Display name** and a **Slug**. The slug takes lowercase letters and numbers, with single hyphens between them, up to 63 characters. It cannot change later.
5. Click **Create tenant**.
6. Ask your IT administrator to add the tenant's people, and give them its slug. IT follows [Adding people to a tenant](../../icbs-install/03-control-plane/10-identity-service/02-Register-Clients-and-Tenants.md#creating-a-tenant-by-hand).

For now, the web app cannot add people to a tenant created here. Skip Steps 3 and 4: give IT the emails of your tutorial user, your pilot lead and the rest of the team (your CSV file) to add, then continue at [Step 5](#step-5--make-the-pilot-lead-a-tenant-administrator) once they appear in the tenant's **Members** section as **Pre-registered**.

See [Tenants](../../users/admin-guide/tenants.md) for editing, the default tenant, and suspension.

---

## Step 3 — Add your tutorial user

For a tenant you created in Step 2, IT adds people instead; skip to [Step 5](#step-5--make-the-pilot-lead-a-tenant-administrator).

You cannot add someone who already belongs to another tenant.

1. Open the list of people in the tenant:
   - As a Platform Administrator, on the Platform Admin Console's **Tenants** page, click the new tenant's name and use its **Members** section.
   - As a Tenant Administrator, open **Settings** > **Admin** > **Users**. People you add join your tenant.
2. Click **Add user**.
3. Enter the tutorial user's **Email**, **First Name** and **Last Name**.
4. Click **Add user** in the dialog.

The tutorial user appears under **Pre-registered** until they first sign in.

Add your pilot lead the same way. Skip this only if you are the pilot lead and already belong to the tenant.

See [Add a person](../../users/admin-guide/user-management.md#add-a-person).

---

## Step 4 — Pre-register the rest of the team from a CSV file

1. In the same list, click **Add from CSV**.
2. Click **Choose CSV file** and pick your file.
3. Check the preview. It lists rows that will be skipped, such as an invalid or repeated email address, with the reason.
4. Click **Pre-register N people**, where N is the number of rows to import.
5. When the run finishes, review each row's outcome and click **Close**.

Importing the same file again is safe; rows already done come back as **Already a member**.

See [Pre-register people from a CSV file](../../users/admin-guide/user-management.md#pre-register-people-from-a-csv-file) for the file format, limits, and outcomes.

---

## Step 5 — Make the pilot lead a Tenant Administrator

1. In the same list, find the pilot lead's row.
2. Open the row's **⋮** menu and choose **Make tenant admin**.
3. Read the confirmation and click **Grant**.

The pilot lead can now manage the tenant's people and agents. Do not grant an administrator role to the tutorial user; the last step checks that a member without one can run jobs.

See [Grant an administrator role](../../users/admin-guide/administrators.md#grant-an-administrator-role).

---

## Step 6 — Have members sign in

Ask the tutorial user and the pilot lead to sign in. In the list of people, watch them move from the **Pre-registered** tab to **Active**.

Tool access, control tags, and keys become available for a person only after they first sign in.

If someone sees **You don't have access to Istari yet**, see [When someone cannot sign in](../../users/admin-guide/user-management.md#when-someone-cannot-sign-in).

---

## Step 7 — Connect an agent to the tenant

An agent runs jobs only for its own tenant. Generating a key creates the agent in the tenant of the administrator who generates it. So this step must be done by an administrator who belongs to the new tenant: the pilot lead, or you if you belong to it. If you do not, ask the pilot lead to do it.

1. Signed in as that administrator, click **Agents** in the main sidebar.
2. Click **Generate Key**.
3. In the **New Agent Access Key** dialog, optionally enter a name in **Agent name (optional)**, and click **Generate**.
4. Click **Download credentials**. The key is shown only once.
5. Send the file securely to IT for [agent configuration](../../icbs-install/04-Agents/05-Agent-Configuration.md#required-configuration-values).

The agent appears on **All Agents** once IT starts it, and **Open Spreadsheet** then appears under its installed modules.

If IT has shell access to the agent host, they can instead create and configure the agent there with the Istari CLI command [`stari key create-agent --configure`](../../icbs-install/05-CLI/05-creating-a-new-agent-with-a-key.md), so the private key never leaves the host.

See [Generate an agent key](../../users/admin-guide/agents.md#generate-an-agent-key).

---

## Step 8 — Grant tool access

People can run functions only for tools you grant them.

1. In the list of people, click **Edit user** (pencil) on the tutorial user's row.
2. Open the **Tool access** tab.
3. Search for **Open Spreadsheet** with **Add tool** and pick it. A new tool starts with every function selected.
4. Clear any functions the team does not need. Keep `@istari:extract`.
5. Click **Save**.

Repeat for each member who will run jobs in the pilot.

See [Manage tool access for a user](../../users/admin-guide/user-management.md#manage-tool-access-for-a-user).

---

## Step 9 — Verify job execution as your tutorial user

Sign in as the tutorial user, not as an administrator, and run one extraction. This is the same check Web app 101 depends on.

1. Click **Resources** in the sidebar.
2. Upload `Group3-UAS-Requirements.xlsx`.
3. Open the file and click **Create Job**.
4. Select **open_spreadsheet** → `@istari:extract`.
5. Click **Execute Function**.
6. Wait until the job status is **Completed**.

If the job **Failed**:

- Sign in as an administrator of the tenant and open **Agents** → **All Agents**. Confirm an agent has a green [status badge](../../users/admin-guide/agents.md#status-badges), such as **Idle**, and lists **Open Spreadsheet** under **Installed Modules**. An agent in another tenant does not run this tenant's jobs.
- On the tutorial user's **Tool access** tab, confirm **Open Spreadsheet** is granted with `@istari:extract` selected.

---

## What you set up

| Piece                    | Why it matters                                                                                    |
| ------------------------ | ------------------------------------------------------------------------------------------------- |
| **Tenant and members**   | Each person belongs to one tenant and signs in with their own account rather than a shared login. |
| **Tenant Administrator** | The pilot lead can add people and manage agents without a Platform Administrator.                 |
| **Connected agent**      | Jobs run on an agent in the same tenant; IT installs and starts it.                               |
| **Tool access**          | Even with an agent, people cannot run functions you have not granted.                             |
| **Verified extract**     | Proves the full path works before the team opens Web app 101.                                     |

## What's next

- **[Web app 101](../connect-build-share/platform-101.md)**: your tutorial user can start the end-user digital thread tutorial.
- **[Python Client 202](../automate/202-python-client.md)**: script the same extract flow once jobs work in the web app.
- **[Administrator Guide](../../users/admin-guide/index.md)**: reference for every admin section, including integrations, secure connections, infosec levels, and agent pools.
- **[Configure partner sharing (Secure Connections)](configure-partner-sharing.md)**: set up Sending and Receiving Connections so Editors can share models with partner organizations.

---

## On installations without tenant management

If Settings has a **Password** entry, your installation does not use tenant management. There are no tenants, no Platform Admin Console, and no Platform Administrators or Tenant Administrators to set up. Follow these steps instead of Steps 1 to 8.

You need:

- administrator access, which your IT administrator grants in your sign-in provider;
- IT available to start an agent with the **Open Spreadsheet** module;
- a tutorial user;
- the sample spreadsheet above.

1. **Confirm admin access.** Click your avatar, then **Settings**, and check for an **Admin** group in the Settings navigation. If it is missing, ask your IT administrator to grant you administrator access.
2. **Connect an agent.** If the top-right button on **Agents** reads **Generate Token**, follow [Generate an agent token](../../users/admin-guide/agents.md#generate-an-agent-token) instead. Otherwise:
   1. On **Agents**, click **Generate Key**.
   2. Download the credentials and send the file securely to IT.
   3. The agent appears on **All Agents** once IT starts it.
3. **Add users.**
   1. Open **Settings** > **Admin** > **Users** and pick your organization in the dropdown.
   2. Click **Add user**, enter **Email**, **First Name** and **Last Name**, and click **Invite**.
   3. New users appear under **Invited** until they accept and sign in, then move to **Active**.
4. **Grant tool access.** Once the tutorial user is **Active**, click **⋯** on their row, then **Manage Tool Access**. Expand **Open Spreadsheet**, enable `@istari:extract`, and click **Save**.
5. **Verify job execution** as in [Step 9](#step-9--verify-job-execution-as-your-tutorial-user).

See [On installations without tenant management](../../users/admin-guide/user-management.md#on-installations-without-tenant-management) in User Management for the rest of the **Users** page.


---

<!-- Source: automate/201-first-system.md -->

---
sidebar_position: 1
sidebar_label: "201 - Your first System"
title: Python Client 201
---

# Python Client 201: Your first System

<div style="position: relative; box-sizing: content-box; max-height: 80vh; max-height: 80svh; width: 100%; aspect-ratio: 1.78; padding: 40px 0 40px 0;">
  <iframe src="https://guides.istaridigital.com/embed/cmtvbg5su01ejuu0kebzslj6t?embed_v=2&utm_source=embed" loading="lazy" title="Guided Walkthrough: 201 Python Tutorial (Voiceless)" allow="clipboard-write" frameborder="0" webkitallowfullscreen="true" mozallowfullscreen="true" allowfullscreen style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"></iframe>
</div>

<!-- AGENT: Credentials in istari.env. Three commands below. Then report system_id and ui_link. Do not explore the SDK first. Never ask for a key or token in chat. -->

Get a documented CAD model onto the Istari Digital Platform: a new **System** whose `baseline` branch holds a README, the model, and the Python that generated it. The same commands work for an AI coding agent.

Everything here runs from one commented script: you download it, it makes the API calls for you, and it prints the ids and a link to what it created. There is no Python to write yet — the point is to have a real System you can click through, and a worked example you can read, before you start writing code of your own.

**Time:** about five minutes — the script itself takes under a minute; the rest is installing the client and generating a Key.

When you want to make the calls yourself, [Python Client 202](./202-python-client.md) is the hands-on lesson: you write the script, register a spreadsheet, run two extractions, and compare what changed.

## What the script does

One run, five steps:

1. **Connects** using the credential named in `istari.env` — nothing is passed on the command line.
2. **Generates the demo files** in your working directory: it writes `make_bracket.py`, runs it to produce `bracket.stl`, and composes `00-README.html`. With `--file` it uploads your CAD instead of generating one.
3. **Uploads each file as a Resource** of type `model`, so the web app renders it in place instead of offering a download.
4. **Creates a System** and commits all three Resources onto its `baseline` branch, in this layout:

| Where it lands             | What it is                                                                                 |
| -------------------------- | ------------------------------------------------------------------------------------------ |
| `00-README.html` (root)    | Front door. Click it — it renders in place and links to the two files below.               |
| `10-Model/bracket.stl`     | Parametric mounting bracket (binary STL). The web app shows STL as an interactive 3D view. |
| `10-Model/make_bracket.py` | Pure-Python generator for the STL.                                                         |

5. **Prints JSON** with `system_id`, each Resource id, and `ui_link` — the web-app URL to open.

The README is versioned **on the branch** with the model, and its links keep working when you upload a new revision. Change the folder with `--folder`.

## Download

- <a href="/quickstart/istari_quickstart_13_2_1.py" download target="_blank" rel="noopener noreferrer">istari_quickstart_13_2_1.py</a> — commented `istari-digital-client` calls. Prints no credential.

The filename names the client version the script is written for: install exactly `13.2.1`. Every run prints the client version it found and stops if it is a different one, so a stale environment fails before it creates anything. Pass `--any-sdk-version` to run against another version anyway.

SHA-256: `0ae71c50fe33a48e71ac171cb562ff0a1b345dfe0c51495f71d00483c490dc61` — compare with `shasum -a 256 istari_quickstart_13_2_1.py` (macOS/Linux) or `Get-FileHash istari_quickstart_13_2_1.py` (PowerShell), or read the file before running it.

## Credentials

Python 3.10 or newer (`python3 --version`), network access, and a credential. In an empty folder, create `istari.env`:

```text
ISTARI_DIGITAL_API_URL=<API URL from Endpoints on Developer Settings>
ISTARI_CLIENT_IDENTITY_SERVICE_SECRET_FILE=/absolute/path/to/the-key-file-you-downloaded.json
ISTARI_DIGITAL_IDENTITY_SERVICE_ENABLED=true
```

Write all three lines, with the names exactly as shown — including the `ISTARI_CLIENT_` prefix on the key path, and the last line, which turns Key authentication on. The client reads them from the environment, so no credential is passed on the command line.

To get the key file: in the Istari Digital web app, open **avatar → Settings → Developer** (example: `https://demo.istari.app/settings/developer`). Click **Generate Key**, then **Download credentials** — the key is shown once. Details: [Developer Settings — Keys](../../users/user-guide/settings.md#developer-settings--keys).

Personal Access Tokens are **deprecated** (July 2026). If your instance still issues one, replace the three lines above with `ISTARI_REGISTRY_URL` and `ISTARI_REGISTRY_AUTH_TOKEN`.

Keep `istari.env` and the key file on disk. Do not put a key or token on the command line, in a prompt, or in a chat log.

## Three commands

### macOS / Linux

```bash
pip install "istari-digital-client==13.2.1"
curl -fsSL https://docs.istaridigital.com/quickstart/istari_quickstart_13_2_1.py -o istari_quickstart_13_2_1.py
python istari_quickstart_13_2_1.py --sample bracket --env istari.env
```

### Faster: with `uv`

```bash
uv venv && source .venv/bin/activate && uv pip install "istari-digital-client==13.2.1"
curl -fsSL https://docs.istaridigital.com/quickstart/istari_quickstart_13_2_1.py -o istari_quickstart_13_2_1.py
python istari_quickstart_13_2_1.py --sample bracket --env istari.env
```

### Windows (PowerShell)

```powershell
py -m pip install "istari-digital-client==13.2.1"
Invoke-WebRequest https://docs.istaridigital.com/quickstart/istari_quickstart_13_2_1.py -OutFile istari_quickstart_13_2_1.py
py istari_quickstart_13_2_1.py --sample bracket --env istari.env
```

If you already downloaded the script from the link above, skip `curl` / `Invoke-WebRequest`. On a local or on-prem docs site, replace `https://docs.istaridigital.com` with that origin.

The last command prints one line per step, then the JSON. Open `ui_link`. To upload your own CAD instead of the sample bracket, swap `--sample bracket` for `--file path/to/your_part.stl`; `python istari_quickstart_13_2_1.py --help` lists every flag.

To check the credential before creating anything, run `python istari_quickstart_13_2_1.py --check-connection --env istari.env`. It authenticates and prints your user id and up to five visible System ids.

## Clean up

Archive the System and every model the run put on its `baseline` branch. Nothing is deleted — restore from the web app:

```bash
python istari_quickstart_13_2_1.py --env istari.env --archive <system_id>
```

## What the script uses

`istari-digital-client` 13.2.1, the client this doc set is written against. Every call it makes is commented in the file. [SDK setup](../../icbs-build-python/01-setup.md) is the reference for your own code, [Folders in a System](../../icbs-build-python/06-folders.md) explains the folder paths, and [Linking to the web app](../../icbs-build-python/linking-to-the-web-app.md) explains the URLs it prints. Install the agent recipes from [Get started with Claude Code](../../icbs-build-ai/get-started-with-claude-code.md).

## Where next

- **[Python Client 202 — First digital thread](./202-python-client.md)** — write the script yourself, run an extraction, and diff two revisions.
- **[V3Client quick start](../../icbs-build-python/v3/01-quick-start.md)** — the unified resource API.


---

<!-- Source: automate/202-python-client.md -->

---
sidebar_position: 2
sidebar_label: "202 - First digital thread"
title: Python Client 202
slug: 202
---

# Python Client 202: Scripting Your First Digital Thread

<div style="position: relative; box-sizing: content-box; max-height: 80vh; max-height: 80svh; width: 100%; aspect-ratio: 1.78; padding: 40px 0 40px 0;">
  <iframe src="https://guides.istaridigital.com/embed/cmtvcbhnc002zup0jh1n4ze68?embed_v=2&utm_source=embed" loading="lazy" title="Guided Walkthrough 202 Python Tutorial" allow="clipboard-write" frameborder="0" webkitallowfullscreen="true" mozallowfullscreen="true" allowfullscreen style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"></iframe>
</div>

In [Web app 101](../connect-build-share/platform-101.md) you used the browser to register a spreadsheet, run an extraction, add a new version, extract again, and **compare** what changed.
Here you will do the same flow with a **short Python script**: one run registers the file, runs two extractions, saves the extracted `named_cells` from each pass, and produces a **comparison** — same files, same revisions you see in the web app, but now the extracted data is in your hands as JSON to apply your own logic.

**Run the script end to end once** (it only takes a minute or two to finish).
**Then** use [Understand the script](#understand-the-script) for a short walkthrough of each part and how it maps to the Istari Digital Platform.

By the end you will know how to:

- **Connect** to the Istari Digital Platform from Python (acting as your user)
- **Register** a file, **run** a job, and **wait** for it to finish
- **List and download** extracted resources
- **Add a new revision** and run extraction again
- **Apply your own logic** to the extracted data while staying anchored to the file and revisions recorded by the Istari Digital Platform

**Time:** ~15 minutes.

## Prerequisites

- An Istari Digital Platform account. If you don't have one yet, follow the [Sign-up Guide](../../users/account/sign-up.md).
- Work through [Web app 101](../connect-build-share/platform-101.md) first so the web app ideas (model, job, resources, versions) feel familiar.
- A **Key** for the Istari Digital Platform instance you are using. The same `istari.env` as [Python Client 201 — Your first System](./201-first-system.md) works here. Details: [Developer Settings — Keys](../../users/user-guide/settings.md#developer-settings--keys). Personal Access Tokens are deprecated (July 2026).
- An agent with the **Open Spreadsheet** integration and access to run `@istari:extract` — your admin can confirm; see [Manage Tool Access](../../users/admin-guide/user-management.md#manage-tool-access-for-a-user).

**Sample files** (same as Web app 101). Save both in a folder, for example `~/uas-tutorial/`:

- <a href="/files/microsoft_office_excel/Group3-UAS-Requirements.xlsx" download target="_blank" rel="noopener noreferrer">Group3-UAS-Requirements.xlsx</a> — baseline
- <a href="/files/microsoft_office_excel/Group3-UAS-Requirements-v2.xlsx" download target="_blank" rel="noopener noreferrer">Group3-UAS-Requirements-v2.xlsx</a> — after a design change

## Set up your environment

### Install the client

We use [uv](https://docs.astral.sh/uv/) so the same commands work on every machine:

```bash
curl -LsSf https://astral.sh/uv/install.sh | sh   # if you do not have uv yet
mkdir -p ~/uas-tutorial && cd ~/uas-tutorial

# Pin the venv to a Python version supported by the client.
# If 3.12 is not already on your machine, uv downloads a managed CPython build for you.
uv venv --python 3.12
source .venv/bin/activate       # macOS / Linux
# .venv\Scripts\Activate.ps1    # Windows (PowerShell) — uncomment instead of the line above

uv pip install "istari-digital-client>=13.2.1"
```

:::note Why pass `--python`?
`uv venv --python 3.12` tells `uv` to use Python 3.12 specifically; if it is not on your machine, `uv` downloads a managed CPython build and uses that — no `pyenv` or `brew install python` needed.

If you'd rather use `python -m venv` or `pyenv` directly, make sure your interpreter matches the [client's supported versions](../../icbs-build-python/01-setup.md#requirements).
:::

<!-- beta-caveat: delete this line and the comment above it when the client drops the Beta notice on the Istari clients -->

> **Beta:** the `Istari` client this tutorial uses is marked Beta in `istari-digital-client`, so its method names can change between releases. This tutorial was tested with 13.2.1 through 13.3.3; if a later release changes a method name, install one of those versions.

### Point the script at your Istari Digital Platform

Create `istari.env` in `~/uas-tutorial/` (reuse the file from [Python Client 201](./201-first-system.md) if you already have one):

```text
ISTARI_DIGITAL_API_URL=<API URL from Endpoints on Developer Settings>
ISTARI_CLIENT_IDENTITY_SERVICE_SECRET_FILE=/absolute/path/to/the-key-file-you-downloaded.json
ISTARI_DIGITAL_IDENTITY_SERVICE_ENABLED=true
```

These are the names `Configuration` reads on its own, so your own scripts can call `Configuration()` with no arguments and let `istari.env` decide the credential:

- `ISTARI_CLIENT_IDENTITY_SERVICE_SECRET_FILE` takes the `ISTARI_CLIENT_` prefix. It is the only name read for the key, and there is no unprefixed fallback. `tutorial202_2026_10.py` also accepts the older unprefixed `IDENTITY_SERVICE_SECRET_FILE` if you already have an `istari.env` from an earlier tutorial.
- With the key path and `ISTARI_DIGITAL_API_URL` set, Key authentication turns on even when `ISTARI_DIGITAL_IDENTITY_SERVICE_ENABLED` is absent. The sample sets it to `true` so the choice is explicit. Set it to `false` to keep a personal access token while a key file is also present. A token on its own uses `ISTARI_REGISTRY_URL` and `ISTARI_REGISTRY_AUTH_TOKEN`.

In the Istari Digital web app, open **avatar → Settings → Developer** (example: `https://demo.istari.app/settings/developer`). Click **Generate Key**, then **Download credentials** — the key is shown once.

Keep `istari.env` and the key file on disk. Do not put a key or token on the command line, in a prompt, or in a chat log.

## Run the script

1. In the folder that contains the two `.xlsx` files, save <a href="/files/tutorials/tutorial202_2026_10.py" download target="_blank" rel="noopener noreferrer">tutorial202_2026_10.py</a> there (easiest). The full script is also reproduced below in [Full script](#full-script) if you prefer to read it inline or paste into a new file.
2. The script uses **`assert`** to catch a few common mistakes up front: the two `.xlsx` files must be in the current folder, and `istari.env` (or the environment) must name a Key. If Python raises `AssertionError`, read the message—it points to what to fix.
3. Run:

```bash
python tutorial202_2026_10.py
```

## What you should see

**In the terminal:** Resource and revision IDs, two jobs that reach **Completed**, two lists of resources, then writes of `named_cells_rev1.json` and `named_cells_rev2.json`,
followed by a **summary** (file name, resource id, **from** / **to** revision ids and times) and a **Change detail** list of the named cells that changed (`name: before → after`)
— output tied to the same **source of truth** as the Istari Digital Platform, not just anonymous JSON.

A trimmed example of what scrolls past (your IDs, timestamps, and values will differ):

```text
Connected.
Resource registered: Group3-UAS-Requirements.xlsx
  Resource ID:  1e59b631-bf46-4945-ad4e-271803add9d2
  Revision ID:  c4a5b113-72b0-4666-b351-0b1cdfea4f61

Job: 8e092739-3712-4434-8f3c-659f99ebfc00
  status: Pending
  status: Running
  status: Completed
First extraction done.

Resources produced by job 1 (6 items):
  - named_cells.json     (resource=4ab2…  rev=9f01…)
  - worksheet_data.json  (resource=5cd3…  rev=a112…)
  - workbook.html        (resource=6ef4…  rev=b223…)
  - …workbook.pdf, workbook.xlsx, run logs (each with their own resource/rev ids)

Saved …/named_cells_rev1.json  (rev 9f01…)

Second file uploaded (new revision).
  Resource ID:  1e59b631-bf46-4945-ad4e-271803add9d2  (same as before)
  Revision ID:  279cd9f6-cbc8-4dc4-8205-07a64df4e9e1

Job: b98c655d-a080-4925-ab4d-b3b4b61d00e2
  status: Claimed
  status: Running
  status: Completed
Second extraction done.

Resources produced by job 2 (6 items):
  - named_cells.json     (resource=4ab2…  rev=c334…)   ← same resource id as run 1, new revision id
  - worksheet_data.json  (resource=5cd3…  rev=d445…)   ← same resource id, new revision
  - …same kinds as above; in each row resource= matches run 1 and rev= is new

Saved …/named_cells_rev2.json  (rev c334…)

--- Summary (resource and revisions from the Istari Digital Platform) ---

Named parameters (named_cells) for 'Group3-UAS-Requirements.xlsx' — resource 1e59b631-bf46-4945-ad4e-271803add9d2
  from: revision c4a5b113-72b0-4666-b351-0b1cdfea4f61 · 2026-04-24T00:27:43+00:00 · user 80ddca7e-…
  to:   revision 279cd9f6-cbc8-4dc4-8205-07a64df4e9e1 · 2026-04-24T00:28:08+00:00 · user 80ddca7e-…

Change detail (8 named cell(s) changed):
  - SubTitle: …Rev A… → …Rev B…
  - control_signal_value: 60000.0 → 80000.0
  - cruise_speed_value: 100.0 → 110.0
  - max_weight_value: 275.0 → 310.0
  - position_accuracy_value: 2.5 → 1.5
  - range_value: 1000.0 → 1200.0
  - temperature_value: -10 to +45 → -20 to +50
  - video_tx_value: 50000.0 → 80000.0
```

The **two `Resource ID` lines are identical** while the **`Revision ID`s differ** — the same logical file moving through two versions, exactly as you'd see on the **Versions** tab in the web app.
The same pattern shows up for each output: in the per‑job listings, the `resource=` of `named_cells.json` (and every other output name) **matches across run 1 and run 2**, while the `rev=` is **new in run 2** — proof that the second extraction added a new revision **to the same output resource**, not a brand‑new one.
The **Change detail** list is what a developer ultimately wants: just the named cells whose values moved between those two revisions.

**On disk** (next to the script and spreadsheets):

| File                    | Role                                                              |
| ----------------------- | ----------------------------------------------------------------- |
| `named_cells_rev1.json` | Snapshot of named cells after the **first** revision’s extraction |
| `named_cells_rev2.json` | Snapshot after the **second** revision’s extraction               |

### Verify in the web app

The script and the web app are not parallel demos: they act on the **same** files. Sign in to the **same** org as your Key, then check:

1. **Resources** — Open the registered Resource (e.g. `Group3-UAS-Requirements`) and match **Resource ID** and revision IDs to the script output.
2. **Versions** — Two revisions (baseline and updated spreadsheet), as in Web app 101.
3. **Activity** or **Jobs** — Two **Completed** extractions for `open_spreadsheet` / `@istari:extract`.
4. **Resources** — Extraction outputs including `named_cells.json`, as in [Web app 101 — Step 4](../connect-build-share/platform-101.md#step-4--explore-the-extracted-resources).
5. **Compare versions** — [Step 7 in Web app 101](../connect-build-share/platform-101.md#step-7--compare-what-changed): use the in-app **File Comparison**; your terminal diff is the same story using the saved JSON files.

**Note:** Each list shows only what **that job** produced. A job's outputs are tied to the revision it ran on, so the script reads that revision's `produces` relationships rather than everything that ever landed on the resource. The script also saves a copy of the first `named_cells` to disk _before_ the new spreadsheet is added, so the comparison stays valid even after the second extraction adds a new revision of the same output.

## Understand the script

`tutorial202_2026_10.py` first **asserts** that the sample spreadsheets and credentials are present.
The numbered steps then follow the same story as [Web app 101](../connect-build-share/platform-101.md):
connect → register → first extraction → save a snapshot of `named_cells` → add a new file revision → second extraction → save `named_cells` again → compare.

### Connect

`Configuration` takes **`digital_api_url`**, **`identity_service_secret_file`**, and **`identity_service_enabled=True`**.
The script loads those from `istari.env` in the current folder (or from the environment), so the Key file stays on disk and nothing secret lives in the code.
`Istari(configuration)` is the client for everyday work: its `resources` manager handles files and their revisions, and its `jobs` manager runs and monitors jobs.
See [Python Client 201](./201-first-system.md) for a first System in one command, or [SDK setup](../../icbs-build-python/01-setup.md) for defaults and other patterns.

A **Key** is **issued to a user**: the client acts **on your behalf**—the same as when you are signed in—so register, list, and job calls use **your** authorization to read files, run tools and functions, and work only where your account is allowed.
If something is denied in the API, the usual causes are the same as in the web app.
See [Sharing and access](../../users/user-guide/sharing-and-access.md) for how access to files and actions is decided.

### Check the connection

`client.resources.list(size=1).first()` is one small authenticated read.
It confirms the Istari Digital Platform is reachable and your Key is accepted, and the script stops with a pointer to the credential help if it is not.
From client 13.3.0, `client.readiness()` also reports the platform's own dependency checks.

### Register the resource and run the first extraction

- `client.resources.create(path, "model")` uploads the spreadsheet and registers it as a **resource** with a stable `resource_id`. `"model"` is the resource type, so the web app can run tools on it.
- `client.jobs.create(resource_id=..., function=..., tool_name=...)` queues `@istari:extract` for `open_spreadsheet` and returns immediately with a job id and an initial status.
- `wait_for_job(client, job)` is a tiny helper defined at the top of the script: it calls `client.jobs.get(job.id)` every 5 seconds, prints `job.status_name` on each loop, and returns the **refreshed** job once it reaches `Completed`. It raises if the job ends in `Failed` or `Canceled`, and it retries server errors and dropped connections itself, because the client does not retry them. Depending on how fast the agent runs you'll see one or several of the states listed in the [`JobStatusName` reference](../../icbs-build-python/api_reference/08-enums.md#jobstatusname). If you do not need the printing, `job.poll()` blocks until the job is done and returns the final status.

The script then calls `outputs_of(client, resource.file_revision_id)` once. A job's outputs are tied to the revision it ran on by `produces` relationships, so the helper lists that revision's relationships, keeps the ones that start at it and do not end at a job, and fetches each written revision with `client.resources.revisions.get(resource_id, revision_id)`.
That is the exact revision the agent wrote — the same `resource_id` and revision id you'd see in the web app — so we can print its name and ids and capture the one named `named_cells.json`.
Its bytes are saved to `named_cells_rev1.json`.

### Add a new revision and run the second extraction

`client.resources.revisions.create(resource_id, path)` adds a new revision to **the same resource** — same id, new revision id.
A second `jobs.create` runs, `wait_for_job` polls until it completes, and the script reads the new outputs the same way from the new revision, writing the second `named_cells` output to `named_cells_rev2.json`.

### Compare

As in [Web app 101 — Compare versions](../connect-build-share/platform-101.md#step-7--compare-what-changed), the goal is to see what changed between two **revisions**.
The extracted `named_cells` is **JSON** in a **vendor‑neutral** shape, so you are free to apply whatever **custom logic** fits the question — here a small dictionary `name → value` per revision, then a flat `name: before → after` line for any name where the value differs.
The same JSON could feed a chart, a filter on specific named ranges, or a short narrative drafted by an **LLM**.

The **Summary** block above the table prints the file name, resource id, and the **from** and **to** revision ids (plus time and uploader).
Whether the change description is written by code or by a model, it stays grounded in the **immutable source of truth** the **Istari Digital Platform** already recorded: the file and the specific revisions it came from.

## What you learned

The same actions you took in the web app in [Web app 101](../connect-build-share/platform-101.md), now from Python — on the same files, with the same model and revision ids.

The calls above come from the `Istari` client's `resources` and `jobs` managers. Older scripts use the flat `Client` methods `add_model`, `update_model`, `add_job`, `get_job`, and `get_artifact`; those are deprecated, and the table shows what replaces each one here.

| Concept                            | Web app 101            | Python (this tutorial)                                                                                                                                                               |
| ---------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Register a file**                | Drag and drop          | `client.resources.create(path, "model")`                                                                                                                                             |
| **Run an extraction**              | Create job in web app  | `client.jobs.create(resource_id=..., function=...)` then poll with `client.jobs.get(job.id)`                                                                                         |
| **Monitor a job**                  | Activity               | `client.jobs.get(job.id).status_name` in a loop                                                                                                                                      |
| **See what a job produced**        | Resources tab on a Job | `client.resources.relationships.list(revision_id)` → `client.resources.revisions.get(resource_id, revision_id)`                                                                      |
| **Download a resource**            | Click Download         | `revision.read_bytes()` → file on disk                                                                                                                                               |
| **Upload a new version**           | Add version            | `client.resources.revisions.create(resource_id, path)`                                                                                                                               |
| **Custom logic on extracted data** | Compare versions       | Any Python you like (`name: before → after` list here; could be a chart, filter, LLM narrative…) on the JSON, anchored to file / model / revision ids in the Istari Digital Platform |

## Full script

The complete `tutorial202_2026_10.py` for reference. You can also <a href="/files/tutorials/tutorial202_2026_10.py" download target="_blank" rel="noopener noreferrer">download it</a>.

```python
"""Tutorial 202: register a spreadsheet, extract data twice, compare named_cells JSON locally."""

import json
import os
import sys
import time
from pathlib import Path

from istari_digital_client.sdk import (
    APIConnectionError,
    Configuration,
    InternalServerError,
    Istari,
    IstariError,
)
from urllib3.exceptions import MaxRetryError

CREDENTIALS_HELP = (
    "Put ISTARI_DIGITAL_API_URL and ISTARI_CLIENT_IDENTITY_SERVICE_SECRET_FILE in istari.env "
    "(or the environment). Developer Settings → Keys: copy the API URL and the path "
    "to the downloaded key file — not the key contents. See Python Client 201 or 202."
)
# The name istari-digital-client itself reads for the Key. The unprefixed
# IDENTITY_SERVICE_SECRET_FILE is still accepted so older istari.env files keep working.
SECRET_FILE_NAMES = ("ISTARI_CLIENT_IDENTITY_SERVICE_SECRET_FILE", "IDENTITY_SERVICE_SECRET_FILE")


def _env(*names: str) -> str:
    for n in names:
        v = (os.environ.get(n) or "").strip()
        if v:
            return v
    return ""


def _load_istari_env(path: str = "istari.env") -> None:
    """Load KEY=VALUE lines without overwriting variables already set in the shell."""
    p = Path(path)
    if not p.is_file():
        return
    for line in p.read_text().splitlines():
        line = line.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        k, _, v = line.partition("=")
        os.environ.setdefault(k.strip(), v.strip().strip("'").strip('"'))


def _on_uncaught(exc_type, exc, tb):
    if issubclass(exc_type, (APIConnectionError, MaxRetryError)):
        print("\nCannot reach the Istari Digital Platform.")
        print(CREDENTIALS_HELP)
        sys.exit(1)
    sys.__excepthook__(exc_type, exc, tb)


sys.excepthook = _on_uncaught


def wait_for_job(client, job, interval=5):
    """Poll a job until it reaches a terminal state, printing the status each loop.
    Returns the refreshed Job; raises if it ends in Failed or Canceled.
    Server errors (5xx) and dropped connections are retried silently; the client does not retry them."""
    while True:
        try:
            job = client.jobs.get(job.id)
        except (InternalServerError, APIConnectionError):
            time.sleep(interval)
            continue
        label = job.status_name
        print(f"  status: {label}")
        if label == "Completed":
            return job
        if label in ("Failed", "Canceled"):
            raise RuntimeError(f"Job {job.id} ended in state {label}.")
        time.sleep(interval)


def outputs_of(client, revision_id):
    """The revisions that jobs run on `revision_id` wrote, as (resource_id, revision) pairs.
    Each job output is tied to the revision it ran on by a `produces` relationship."""
    outputs = []
    for edge in client.resources.relationships.list(revision_id):
        written = edge.right_revision
        if (
            edge.relationship_type_name != "produces"
            or edge.left_revision.file_revision_id != revision_id
            or written.owning_entity_type == "job"
        ):
            continue
        resource_id = written.owning_entity_id
        outputs.append((resource_id, client.resources.revisions.get(resource_id, written.file_revision_id)))
    return outputs

baseline_path = Path("Group3-UAS-Requirements.xlsx")
revised_path = Path("Group3-UAS-Requirements-v2.xlsx")

assert baseline_path.is_file(), (
    f"Expected {baseline_path.name!r} in {str(Path.cwd())!r}. "
    "Download the sample from the Python Client 202 tutorial, save it next to this script, then run again."
)
assert revised_path.is_file(), (
    f"Expected {revised_path.name!r} in {str(Path.cwd())!r}. "
    "Download the v2 sample from the same tutorial, same folder, then run again."
)

_load_istari_env()

api_url = _env("ISTARI_DIGITAL_API_URL")
secret_file = _env(*SECRET_FILE_NAMES)
if api_url and secret_file:
    assert Path(secret_file).is_file(), (
        f"{SECRET_FILE_NAMES[0]} points at {secret_file}, which does not exist. "
        "Use the absolute path of the key file you downloaded from Developer Settings."
    )
    configuration = Configuration(
        digital_api_url=api_url,
        identity_service_secret_file=secret_file,
        identity_service_enabled=True,
    )
else:
    registry_url = _env("ISTARI_DIGITAL_REGISTRY_URL", "ISTARI_REGISTRY_URL")
    token = _env(
        "ISTARI_DIGITAL_REGISTRY_AUTH_TOKEN",
        "ISTARI_REGISTRY_AUTH_TOKEN",
        "ISTARI_PERSONAL_ACCESS_TOKEN",
    )
    assert api_url or registry_url, CREDENTIALS_HELP
    assert secret_file or token, CREDENTIALS_HELP
    configuration = Configuration(
        registry_url=registry_url,
        registry_auth_token=token,
    )

# 1. Connect — Key (current) or PAT (deprecated July 2026)
client = Istari(configuration)

# 2. Connection check — one small authenticated read confirms the Istari Digital Platform
#    is reachable and your credential is accepted.
try:
    client.resources.list(size=1).first()
except (IstariError, MaxRetryError) as e:
    print(f"\nCannot reach the Istari Digital Platform ({type(e).__name__}).")
    print(CREDENTIALS_HELP)
    sys.exit(1)
print("Connected.")

# 3. Register the first spreadsheet (same as uploading on Resources)
resource = client.resources.create(baseline_path, "model")
print(f"Resource registered: {resource.name}")
print(f"  Resource ID:  {resource.resource_id}")
print(f"  Revision ID:  {resource.file_revision_id}")

# 4. First extraction — Open Spreadsheet → @istari:extract
job1 = client.jobs.create(
    resource_id=resource.resource_id,
    function="@istari:extract",
    tool_name="open_spreadsheet",
)
print(f"\nJob: {job1.id}")
job1 = wait_for_job(client, job1)
print("First extraction done.")

# 5. Inspect what job 1 produced and capture the named_cells revision.
#    Outputs hang off the revision the job ran on, so this list is job 1's alone.
outputs_1 = outputs_of(client, resource.file_revision_id)
print(f"\nResources produced by job 1 ({len(outputs_1)} items):")
named_cells_rev1 = None
for resource_id, rev in outputs_1:
    print(f"  - {rev.name}  (resource={resource_id}  rev={rev.id})")
    if rev.name and "named_cells" in rev.name:
        named_cells_rev1 = rev

if named_cells_rev1 is None:
    raise RuntimeError("No named_cells.json was produced by the first extraction job.")

path_named_cells_1 = Path("named_cells_rev1.json")
path_named_cells_1.write_bytes(named_cells_rev1.read_bytes())
print(f"\nSaved {path_named_cells_1.resolve()}  (rev {named_cells_rev1.id})")

# 6. Add the second spreadsheet as a new revision of the same resource
new_rev = client.resources.revisions.create(resource.resource_id, revised_path)
print("\nSecond file uploaded (new revision).")
print(f"  Resource ID:  {resource.resource_id}  (same as before)")
print(f"  Revision ID:  {new_rev.id}")

# 7. Second extraction on the current revision
job2 = client.jobs.create(
    resource_id=resource.resource_id,
    function="@istari:extract",
    tool_name="open_spreadsheet",
)
print(f"\nJob: {job2.id}")
job2 = wait_for_job(client, job2)
print("Second extraction done.")

# 8. Same pattern for job 2, on the new revision
outputs_2 = outputs_of(client, new_rev.id)
print(f"\nResources produced by job 2 ({len(outputs_2)} items):")
named_cells_rev2 = None
for resource_id, rev in outputs_2:
    print(f"  - {rev.name}  (resource={resource_id}  rev={rev.id})")
    if rev.name and "named_cells" in rev.name:
        named_cells_rev2 = rev

if named_cells_rev2 is None:
    raise RuntimeError("No named_cells.json was produced by the second extraction job.")

path_named_cells_2 = Path("named_cells_rev2.json")
path_named_cells_2.write_bytes(named_cells_rev2.read_bytes())
print(f"\nSaved {path_named_cells_2.resolve()}  (rev {named_cells_rev2.id})")

# 9. Compare: list named-cell value changes between the two extractions
cells_1 = {c["name"]: c.get("value") for c in json.loads(path_named_cells_1.read_text(encoding="utf-8"))}
cells_2 = {c["name"]: c.get("value") for c in json.loads(path_named_cells_2.read_text(encoding="utf-8"))}
changes = sorted(
    (name, cells_1.get(name), cells_2.get(name))
    for name in set(cells_1) | set(cells_2)
    if cells_1.get(name) != cells_2.get(name)
)


print("\n--- Summary (resource and revisions from the Istari Digital Platform) ---\n")
print(f"Named parameters (named_cells) for {resource.name!r} — resource {resource.resource_id}")
print(f"  from: revision {resource.file_revision_id} · {resource.created.isoformat(timespec='seconds')} · user {resource.created_by_id}")
print(f"  to:   revision {new_rev.id} · {new_rev.created.isoformat(timespec='seconds')} · user {new_rev.created_by_id}")

print(f"\nChange detail ({len(changes)} named cell(s) changed):")
if not changes:
    print("  (no value changes)")
for name, before, after in changes:
    print(f"  - {name}: {before} → {after}")
```

## What’s next

- **Pipelines** — [Sequential pipeline](../../icbs-build-python/02-patterns/01-workflows/01-sequential-pipeline.mdx)
- **Parameter studies** — [Parameter study](../../icbs-build-python/02-patterns/01-workflows/02-parameter-study.mdx)
- **API reference** — [Python client](../../icbs-build-python/api_reference/01-client.md)


---

<!-- Source: automate/203-share-with-partner.md -->

---
sidebar_position: 3
sidebar_label: "203 - Share with a partner"
title: Python Client 203
slug: 203
---

# Python Client 203: Sharing a Resource with a Partner

In [Share with a partner](../../users/user-guide/secure-connections.md) you used the **Share** dialog to send a model to a partner organization over a Secure Connection. Here you do the same from a **short Python script**: find the connections you are allowed to share to, share a resource at the right access level, check what it is shared to, and revoke the share when you are done.

This is the pattern to use when sharing is part of an automated pipeline — for example, publishing every approved revision of a model to a supplier without opening the web app.

By the end you will know how to:

- **List** the Secure Connections a resource may be shared to
- **Share** a resource with a partner at **View** or **Edit** access
- **Check** which partners a resource is currently shared to
- **Revoke** a share

**Time:** ~10 minutes.

## Prerequisites

- An Istari Digital Platform account with a **Key**. Details: [Developer Settings — Keys](../../users/user-guide/settings.md#developer-settings--keys).
- A **Sending Connection** to a partner organization, configured by your administrator. If none exists yet, point your admin at [Configure partner sharing](../administer/configure-partner-sharing.md).
- **Editor** access or above on a model or artifact you are allowed to share externally.
- `istari-digital-client` installed in a virtual environment — see [Install the client](./202-python-client.md#install-the-client) — plus [python-dotenv](https://pypi.org/project/python-dotenv/) to read the credentials file:

  ```bash
  uv pip install python-dotenv
  ```

## What you need to know first

- Secure Connections sync **models and artifacts**. Systems cannot be shared as a whole.
- Sharing calls take the resource's **`file_id`**, the storage-level identifier on `ResourceDto`. Listing shares takes the **`resource_id`**. Both are on every resource the client returns.
- Each connection has a **received access level** ceiling set by the administrator. You can request **Edit** access for the partner only on a connection whose ceiling is **Editor**.
- A connection may also restrict **Permitted Senders**. If it does, you must be on the list, and the per-user level your administrator gave you (View or Edit) caps your `share_level` as well. The SDK has no non-admin call to read that level, so an Edit share above it fails with `403`. Ask your administrator if that happens.
- List calls return one page at a time. Each page carries the cursor for the next page in `next_page`; pass it back as `cursor` until it is `None`.
- Sync is **asynchronous**. Your call returns as soon as the share is recorded; the partner receives the file on the next sync cycle (typically 1–2 minutes).
- The connection's **Permitted Infosec Level**, **Tag Rules**, and **Permitted Senders** are enforced on the server. A share that violates them is rejected, and a later change that makes a shared resource ineligible revokes it automatically.

## Run the script

Create `istari.env` in the folder you will run the script from:

```text
ISTARI_DIGITAL_API_URL=<API URL from Endpoints on Developer Settings>
ISTARI_CLIENT_IDENTITY_SERVICE_SECRET_FILE=/absolute/path/to/the-key-file-you-downloaded.json
ISTARI_DIGITAL_IDENTITY_SERVICE_ENABLED=true
```

These are the three names `Configuration` reads on its own, which is why the script constructs it with no arguments:

- `ISTARI_CLIENT_IDENTITY_SERVICE_SECRET_FILE` takes the `ISTARI_CLIENT_` prefix. It is the only name read for the key, and there is no unprefixed fallback.
- With the key path and `ISTARI_DIGITAL_API_URL` set, Key authentication turns on even when `ISTARI_DIGITAL_IDENTITY_SERVICE_ENABLED` is absent. The sample sets it to `true` so the choice is explicit. An `istari.env` from [Python Client 201](./201-first-system.md) or [202](./202-python-client.md) already has that line. Set the variable to `false` only when that file should keep a personal access token.

Because the credential type is decided entirely by `istari.env`, the script needs no changes to run against a Personal Access Token instead. Replace the three lines above with:

```text
ISTARI_REGISTRY_URL=<registry URL from Endpoints on Developer Settings>
ISTARI_REGISTRY_AUTH_TOKEN=<your personal access token>
```

Keys are the current credential; Personal Access Tokens are deprecated (July 2026). Keep `istari.env` and the key file on disk — do not put a key or token on the command line, in a prompt, or in a chat log.

Save the [full script](#full-script) as `share_with_partner.py` next to `istari.env`, then set two values at the top:

- `RESOURCE_ID` — the `resource_id` of the model or artifact to share. You can copy it from the resource's URL in the web app.
- `CONNECTION_LABEL` — the label of the Sending Connection, as your administrator named it.

```bash
python share_with_partner.py
```

## What you should see

```text
Shareable connections:
  a1b2c3d4-...  partner-registry  ceiling=Editor
  e5f6a7b8-...  supplier-b        ceiling=Viewer

Sharing wing-stress.stl to partner-registry ...
  share id     : 9c8d7e6f-...
  share level  : Viewer

Currently shared to:
  partner-registry (Viewer)

Revoking ...
  is_archived  : True
```

In the web app, open the resource's details pane. The **Connections** row shows the partner while the share is active, and returns to **None** after the revoke.

## Understand the script

### Connect

`V3Client` shares the same `Configuration` as `Client`. `load_dotenv("istari.env")` copies the file's `KEY=VALUE` lines into the environment, and `Configuration()` reads them from there — so there is no credential handling in the script and nothing secret in the code. `load_dotenv` does not overwrite variables already set in the shell, so exported credentials still win in CI.

Pass arguments to `Configuration` only for values you want to override in code. See [SDK setup](../../icbs-build-python/01-setup.md) for every variable it recognizes.

### Find a connection you can share to

`list_shareable_sending_connections()` is available to any authenticated user, not just administrators. It returns each active connection's `id`, `label`, `description`, and `received_access_level`. The script walks every page with `next_page` and matches on `label` to find the partner your administrator set up.

### Share

`share_file_to_sending_remote()` records the share. The optional `ShareFileToSendingRemoteDto` sets `share_level`:

- Omit it, or pass `ReceivedAccessLevel.VIEWER`, for a read-only copy on the partner side.
- Pass `ReceivedAccessLevel.EDITOR` to let the partner run jobs on their copy and add their own revisions. Their edits stay on their copy and are not sent back to you. This needs both the connection's ceiling and, if the connection restricts senders, your own permitted-sender level to be **Editor**.

Calling the method again for the same connection and file is safe. It restores a revoked share, or updates the `share_level` of an active one.

### Check what a resource is shared to

`list_sending_remote_shares_for_resource()` lists the active shares for a resource. Pass `show_archived=True` to include revoked shares as well. Each item is a `RemoteFileShareDto` with the connection id and label, the `share_level`, and `is_archived`.

### Revoke

`revoke_file_from_sending_remote()` stops future synchronization and sends a revocation signal across the connection. The partner's platform tombstones its copy on a later sync cycle. Revocation is best-effort: it cannot remove data the partner already downloaded or exported. See [Revocation and Tombstoned Files](../../users/user-guide/secure-connections.md#revocation-and-tombstoned-files).

The script revokes at the end so you can run it repeatedly against the same resource. In a real pipeline you would leave the share in place and let new revisions sync automatically.

## What you learned

- The **Share** dialog and the SDK use the same sharing model: a share ties one resource's file to one Sending Connection at one access level.
- `list_shareable_sending_connections()` is the non-admin way to discover connections.
- `share_file_to_sending_remote()` and `revoke_file_from_sending_remote()` take the resource's `file_id`; `list_sending_remote_shares_for_resource()` takes the `resource_id`.
- Sharing is recorded immediately and synced asynchronously.

## Full script

```python
"""Share a resource with a partner organization over a Secure Connection.

Credentials come from istari.env. Set RESOURCE_ID and CONNECTION_LABEL before running.
"""

import sys

from dotenv import load_dotenv
from istari_digital_client import Configuration, V3Client
from istari_digital_client.v3.models.received_access_level import ReceivedAccessLevel
from istari_digital_client.v3.models.share_file_to_sending_remote_dto import (
    ShareFileToSendingRemoteDto,
)

RESOURCE_ID = "<resource_id>"          # model or artifact to share
CONNECTION_LABEL = "partner-registry"  # Sending Connection label from your admin
WANT_EDIT_ACCESS = False               # True to let the partner run jobs and add revisions

# 1. Connect. load_dotenv puts istari.env into the environment; Configuration() reads the
#    API URL, key file, and identity-service switch from there, so no credential is in code.
load_dotenv("istari.env")
v3 = V3Client(Configuration())

# 2. List the connections this user may share to (works for non-admins).
#    Walk every page: `next_page` is the cursor for the next one, None on the last.
print("Shareable connections:")
connections = []
cursor = None
while True:
    page = v3.list_shareable_sending_connections(size=100, cursor=cursor)
    connections.extend(page.items)
    cursor = page.next_page
    if not cursor:
        break

for candidate in connections:
    print(f"  {candidate.id}  {candidate.label}  ceiling={candidate.received_access_level}")

connection = next((c for c in connections if c.label == CONNECTION_LABEL), None)
if connection is None:
    sys.exit(f"No active Sending Connection labelled {CONNECTION_LABEL!r}")

# 3. Decide the access level, respecting the connection's ceiling.
share_level = ReceivedAccessLevel.VIEWER
if WANT_EDIT_ACCESS:
    if connection.received_access_level != ReceivedAccessLevel.EDITOR:
        sys.exit(f"{connection.label} only permits view access")
    share_level = ReceivedAccessLevel.EDITOR

# 4. Share — takes the resource's file_id, not its resource_id
resource = v3.get_resource(RESOURCE_ID)
print(f"\nSharing {resource.name} to {connection.label} ...")
share = v3.share_file_to_sending_remote(
    remote_id=connection.id,
    file_id=resource.file_id,
    share_file_to_sending_remote_dto=ShareFileToSendingRemoteDto(share_level=share_level),
)
print(f"  share id     : {share.id}")
print(f"  share level  : {share.share_level}")

# 5. Confirm what the resource is shared to — takes the resource_id
print("\nCurrently shared to:")
shares = v3.list_sending_remote_shares_for_resource(resource_id=resource.resource_id)
for existing in shares.items:
    print(f"  {existing.sending_connection_label} ({existing.share_level})")

# 6. Revoke so the script can be re-run. Drop this step in a real pipeline.
print("\nRevoking ...")
revoked = v3.revoke_file_from_sending_remote(remote_id=connection.id, file_id=resource.file_id)
print(f"  is_archived  : {revoked.is_archived}")
```

## What's next

- [V3Client reference — Secure Connection Sharing](../../icbs-build-python/v3/v3-client.mdx#secure-connection-sharing) for full signatures and return types.
- [Share with a partner](../../users/user-guide/secure-connections.md) for what the partner sees, automatic unsharing, and revocation semantics.
- [Configure partner sharing](../administer/configure-partner-sharing.md) if you are the administrator setting up the connection.


---

<!-- Source: connect-build-share/platform-101.md -->

---
sidebar_position: 1
title: 101 - Web app basics
---

# Web app 101: Your First Digital Thread

<div style="position: relative; box-sizing: content-box; max-height: 80vh; max-height: 80svh; width: 100%; aspect-ratio: 1.80; padding: 40px 0 40px 0;">
  <iframe src="https://guides.istaridigital.com/embed/cmrd7k7r12k5pqmo5m7v1ikhi?embed_v=2&utm_source=embed" loading="lazy" title="Web App 101 Guided Walkthrough" allow="clipboard-write; fullscreen" frameborder="0" allowfullscreen style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"></iframe>
</div>

In this tutorial you use the **Istari Digital web app** to build a small but complete **digital thread**: a traceable chain from an engineering file through its extracted data and a subsequent design revision. You will do so using only your browser, no code, no command line.

The scenario: your team is designing a Group 3 UAS (unmanned aerial system) — a tailless flying-wing drone with a heavy-fuel engine, satellite comms, and a multi-sensor payload. The system requirements and component specifications live in a spreadsheet. You will register that spreadsheet in the web app, extract structured data from it, upload a revised version after a design change, re-extract, and compare the results.

By the end you will understand:

- How files are **registered** in the Istari Digital Platform and why registration matters
- How **functions** and **jobs** turn raw files into structured, shareable resources
- How **versioning** tracks design changes without losing history
- How to **compare** extracted data between revisions to see exactly what changed

**Time:** ~15 minutes.

## Prerequisites

- An Istari Digital Platform account. If you don't have one yet, follow the [Sign-up Guide](../../users/account/sign-up.md).
- An administrator must complete [Onboard your tenant](../administer/onboard-your-organization.md) first — your environment needs a connected agent with **Open Spreadsheet** and your account needs **tool access** for `@istari:extract`.

Download these two files to your computer — they are versions of the same UAS requirements spreadsheet:

- <a href="/files/microsoft_office_excel/Group3-UAS-Requirements.xlsx" download target="_blank" rel="noopener noreferrer">Group3-UAS-Requirements.xlsx</a> — the baseline requirements (v1)
- <a href="/files/microsoft_office_excel/Group3-UAS-Requirements-v2.xlsx" download target="_blank" rel="noopener noreferrer">Group3-UAS-Requirements-v2.xlsx</a> — an updated version after a design review (v2)

:::info What's in the file?
The spreadsheet captures requirements and component specs for a Group 3 UAS — range, cruise speed, payload capacity, weight budgets, and subsystem details (propulsion, power, flight control, comms, payload, airframe). Think of it as the single source of truth for the drone's design parameters. In later tutorials you could use the extracted data to programmatically generate design options with the Python client — but for now, we focus on the web app fundamentals.
:::

---

## Step 1 — Register your first file

Every engineering workflow on the Istari Digital Platform starts with **registration**. When you upload a file, the Istari Digital Platform gives it a unique identity (a UUID) so it can be versioned, shared, and used as input for jobs. To learn more about how registration works, see [Resources](../../intro/key-concepts/resources.md).

That UUID is the anchor of your digital thread. Every extraction, every revision, every share event links back to the same registered identity. In fact, everything in the Istari Digital Platform has a UUID — files, jobs, resources, revisions, systems. This means every action is traceable: you can always answer _who changed what, when, and what was produced from it_. When AI agents or automated workflows operate on your data, these UUIDs are the safeguard — they provide an auditable record of exactly which inputs the AI used and which outputs it produced.

1. Sign in to the web app and click **Resources** in the left sidebar.
2. Click **Add resources** (or drag and drop) and select `Group3-UAS-Requirements.xlsx` from your computer.
3. In the upload dialog, check the **Resource name** — the web app pre-fills it from the file name (e.g. `Group3-UAS-Requirements`). You can leave **External Version** and **Tags** empty for now.
4. Click **Upload**.

Once the upload completes, the file appears in your file list. Click it to open the file detail view. Open the **details panel** on the right (click the details icon in the top-right action bar if it is hidden). You will see metadata the web app created for you: the **Resource ID** (a UUID), **Revision ID**, creation date, and owner. This file is now a **model** — a registered file that can be used as input for Istari Digital Platform functions. Later, when you upload a revised version, the Resource ID stays the same — only a new revision is added. This is how the Istari Digital Platform maintains a complete history without losing the identity.

## Step 2 — Extract structured data

Your spreadsheet is full of valuable data — requirements, component specs, weight budgets — but it is locked inside a file format that requires specific software to open. In this tutorial we use an `.xlsx` file that you could open in Microsoft Excel, but in real projects the situation is often much harder. SysML models, CAD assemblies, and simulation decks each require powerful, specialized tools whose licenses are typically limited to the teams that author the files. A program manager reviewing the design, a supplier checking an interface, or an AI agent running an automated workflow won't always have the right application installed.

This is where extraction changes the game. The Istari Digital Platform uses [integrations](../../intro/key-concepts/integrations.md) to reach into proprietary files and produce **vendor-neutral resources** — JSON, CSV, HTML, PDF, and 3D formats like OBJ. The licensed tool is used once, on the agent, during extraction. From that point on, everyone else on the project can work with the open resources without needing the original software. These open formats are also ideal inputs for automated workflows and AI agents, because structured JSON or CSV can be parsed by any programming language or LLM without proprietary SDKs.

Extraction is a **function** — a specific operation that an [agent](../../intro/key-concepts/integrations.md) executes on your file. The agent opens the file using the appropriate tool, pulls out structured data, and uploads the results as new **resources** attached to your model.

1. Click on your file in the left panel tree. The center panel has tabs that you can explore. The **Preview** tab displays a **"Preview not available"** message because the web app cannot render a raw `.xlsx` file directly.
2. Click the **Create Job** button. The **Create Job** dialog opens.
3. In the dialog, browse the list and select `open_spreadsheet` → `@istari:extract`.

   The dialog only displays the tools available for the type of file selected.
   Depending on the tool, the dialog may also show a **Version** selector (when multiple versions of the tool are available) and **Parameters** to configure (when the function accepts inputs). For `@istari:extract` on Open Spreadsheet, there is nothing extra to fill in. Under **Advanced Options** you can pick a specific **Agent** or **Agent Pool** — leave this on Automatic for now; we will cover agent pools in a later tutorial.

4. Click **Execute Function**.

The Istari Digital Platform creates a **job** — a tracked unit of work — and assigns it to an agent for execution.

## Step 3 — Wait for the job to complete

Every job follows a lifecycle: **Pending → Claimed → Validating → Running → Uploading → Completed**. The Istari Digital Platform manages this lifecycle; you follow progress in the web app in real time.

1. After clicking **Execute Function**, the **Activity** panel at the bottom of the center view shows your job. Watch the **Status** update as the agent picks up and processes the file.
2. Wait for the status to reach **Completed**. Depending on the file and the integration, this can take anywhere from a few seconds to several minutes.
3. Once completed, refresh the page in your browser to make sure the file tree is up to date. Then expand `Group3-UAS-Requirements` in the left panel — you will see the job listed underneath, and below it the resources it produced.

If the status shows **Failed**, check with your administrator that an agent with the Open Spreadsheet module is running and available.

:::tip
You can also monitor all your jobs from the **Jobs** page (click **Jobs** in the left sidebar). It lists every job you have started across all your models, with status, function name, tool, and timestamps — useful when you have multiple jobs running.
:::

## Step 4 — Explore the extracted resources

Once the job completes, the agent uploads the results as new resources attached to your model. Resources produced by a job are called **artifacts** — they are the vendor-neutral, readable outputs of an extraction. Like every other object in the Istari Digital Platform, each artifact has its own UUID and is fully traceable back to the job and model that produced it.

1. In the file tree, expand `Group 3 UAS Requirements` to see the job and its output resources. Click any resource to preview it in the center panel.
2. The exact resources produced depend on the integration and the function you ran — a spreadsheet extraction produces CSVs and JSON, while a CAD extraction (e.g. Siemens NX) might produce PNG views, a 3D OBJ model, and a JSON parameter table. For the UAS requirements spreadsheet, Open Spreadsheet's `@istari:extract` produces the following resources:
   - `Requirements.csv` — the raw tabular data from the Requirements worksheet
   - `Verification Matrix.csv` — the verification traceability matrix
   - `Component BOM.csv` — the bill of materials for UAS components
   - `Mass Budget.csv` — the mass budget breakdown
   - `named_cells.json` — all named ranges and their values as structured key-value pairs
   - `worksheet_data.json` — full structured content of every worksheet
   - `workbook.pdf` — a rendered PDF of the entire workbook
   - `workbook.html` — a browser-viewable HTML version of the workbook
   - `workbook.xlsx` — a copy of the original workbook

   Notice that each worksheet in the spreadsheet becomes its own CSV, so the number of CSV resources scales with the number of sheets.

3. Click any resource to preview it. The web app renders CSV, JSON, PDF, and HTML directly in the browser — no download or external software needed. Notice that you are viewing the contents of the spreadsheet without Microsoft Excel or any other tool installed on your machine. This is the vendor-neutral principle at work.

Take a moment to look at **named_cells.json**. In our use case, the creator of the xlsx file has named some key cells in the spreadsheet which are valuable. **named_cells.json** contains every named cells from the spreadsheet as a structured json document. For our UAS design, this includes information like

- target range (1000 nm),
- maximum structure weight (275 lb),
- cruise speed (100 knots),
- payload capacity (125 lb).

Because the output is plain JSON, any tool can consume it — a Python script, a web dashboard, an AI agent, or a CI/CD pipeline.

The same principle applies to every integration: a Cameo extraction produces JSON representations of SysML blocks, a Creo extraction produces geometry and parameter tables, a Nastran extraction produces simulation results.

Regardless of the source tool, the extracted resources are open and machine-readable.

4. To save any resource to your computer, select it and click **Download**.

## Step 5 — Upload a revised version

Engineering is iterative. After a design review, the program extends the mission profile to include maritime patrol — this requires longer range (1,200 nm instead of 1,000), a higher cruise speed, upgraded imaging sensors, and a reinforced airframe to support the increased gross weight. Instead of uploading a completely new file, you upload a **new version** of the same model. This keeps the full history under one identity.

1. Select `Group3-UAS-Requirements` in the file list.
2. Open the **details panel** on the right side of the screen. If it is hidden, click the details icon in the top-right action bar to toggle it open.
3. Scroll to the **Versions** section at the bottom of the details panel and click the **+** (Add version) button.
4. In the upload dialog, select `Group3-UAS-Requirements-v2.xlsx` from your computer.
5. Click **Upload**.

The Istari Digital Platform adds a new **revision** to the same model. The UUID stays the same, but the revision history now shows two entries. Anyone with access to this file can see both versions and understand the progression.

:::tip
This is a core principle of the digital thread: every change is a new revision, not a new file. The model's identity — and everything linked to it (resources, jobs, sharing permissions, system memberships) — carries forward.
:::

## Step 6 — Extract the new version

Now let's extract data from the updated spreadsheet so we can compare what changed.

1. In the **Versions** section of the details panel, click the version you just uploaded to make it the active revision. The center view updates to show the new version.
2. Click **Create job** in the **Activity** panel at the bottom of the center view.
3. In the **Create Job** dialog, select the same tool and function as before: `open_spreadsheet` → `@istari:extract`.
4. Click **Execute Function** and wait for the status to reach **Completed**. Refresh the page once done.

You will now see a second set of resources under this model, produced by the new job.

## Step 7 — Compare what changed

Now for the payoff. You have two versions of the same spreadsheet and two sets of extracted data. The web app lets you see exactly what changed.

1. In the file detail view, click the **Compare versions** button (the arrow-left-right icon in the action bar). The **File Comparison** dialog opens.
2. Since the file only has two revisions, the dialog automatically selects v1 on the left and v2 on the right. When a file has more revisions, use the **left and right version selectors** at the top of the dialog to pick which two to compare.
3. Toggle between **Split** and **Unified** diff views.

The diff viewer highlights additions, deletions, and modifications. You can see at a glance which parameters changed, which components were updated, and which requirements stayed the same — for example, the fuel capacity increasing from 18 to 22 gallons, the target range moving from 1,000 to 1,200 nm, or the cruise speed rising from 100 to 110 knots.

This is the digital thread in action: a traceable, auditable record of how your design evolved from one version to the next.

## Step 8 (optional) — Clean up

If this was a practice run, you can tidy up your file list by archiving the tutorial file. The Istari Digital Platform is immutable — files are never deleted, so your data and its full history are always preserved. Archiving simply hides the file from the default view.

1. Select `Group3-UAS-Requirements` in the file list.
2. Click the **Archive** button (box icon) in the action bar.
3. Confirm the action in the dialog.

The file disappears from the active list. You can always find it again by opening the **Filter files** panel and setting **Status** to **Archived** or **All**.

---

## What you learned

| Concept                | What happened                                                                                                                                                             |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **File registration**  | Uploading created a tracked model with a unique UUID. Your file content stayed in your own storage — the Istari Digital Platform only records metadata.                   |
| **Functions and jobs** | You ran `@istari:extract` — a function executed by an agent on your model. The job lifecycle (Pending → Completed) was fully tracked.                                     |
| **Resources**          | The extraction produced vendor-neutral resources (CSV, JSON, PDF) from a proprietary format — readable by anyone, any tool, or any AI agent without specialized software. |
| **Versioning**         | Uploading a revised file added a new revision to the same model identity, preserving the full history.                                                                    |
| **Comparison**         | You compared extracted data across versions to see exactly what changed in the design.                                                                                    |

## What's next

You have just completed the fundamental Istari Digital Platform workflow in the web app: **register → extract → revise → compare**. Here is where to go from here:

- **Organize with Systems.** Group related files (the UAS requirements spreadsheet, CAD models, simulation inputs) into a system. Take snapshots to capture consistent, point-in-time states. Compare snapshots across design iterations. See the [Systems Guide](../../users/user-guide/systems.md).
- **Share with your team.** Share files or systems with teammates and assign roles (Viewer, Editor, Administrator). See [Sharing and Access](../../users/user-guide/sharing-and-access.md).
- **Automate with the Python client.** Everything you did in the browser can be scripted — upload files, run jobs, retrieve resources, and build automated pipelines that generate design options from extracted parameters. See [Python Client 201 — Your first System](../automate/201-first-system.md).
- **Explore more integrations.** The Open Spreadsheet integration is just the beginning. The Istari Digital Platform supports CAD tools (CATIA, Creo, NX), simulation solvers (NASTRAN, MATLAB), systems engineering tools (Cameo, SysML), and more. See [Supported Integrations](../../integrations/01-supported-integrations.md).


---

<!-- Source: extend/301-build-module.md -->

---
sidebar_position: 1
sidebar_label: "301 - First module (shell)"
title: Integration 301
---

# Integration 301: Your First Module in 15 Minutes

## Objective

Create a minimal Istari Digital Platform module using **a shell script** — no Python framework, no build tools. The module:

1. Accepts an input text file
2. Copies it to the output with a timestamp header
3. Can be executed through the Istari Digital Platform

By completing this tutorial, you'll learn:

- How to structure a platform module
- How to define function inputs and outputs using the file contract
- How to test, deploy, and run your module
- How to set up and configure the Istari Digital Agent to execute jobs

> **Namespace:** Throughout this tutorial, replace `<namespace>` with your own unique namespace (e.g. your name or team: `alice`, `teamx`). This keeps your module key unique when multiple developers share the same platform control plane.  
> For example: module key `@alice:integration-301`, function key `@alice:copy`, tool key `alice-tool`, tool display name `Alice Tool`.

## Prerequisites

- Access to an Istari Digital Platform environment (e.g. `dev.istari.app`)
- Admin privileges in the Istari Digital Platform, or access to an admin user to perform the required actions on your behalf
- [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/) installed
- The **Istari Digital CLI** — `debian amd64` from the [Customer Portal](https://portal.istari.app)
- The **Istari Digital Agent installer** — `debian amd64` from the [Customer Portal](https://portal.istari.app)

This tutorial was checked on Ubuntu 24.04 in Docker with CLI **0.21.20** and Agent **11.11.2**. Replace `<VERSION>` with the CLI version you downloaded, and `X.Y.Z` with the Agent version.

## Key Concepts

### Modules

A module is a package containing one or more functions that the Istari Digital Agent can execute. Modules are language-agnostic but must follow a file-based contract for communication.

Your module can be **anything executable**: a shell script, a Python script, a compiled binary. In this tutorial we use a simple bash script.

### Functions

Functions define:

- **Inputs**: Models (files) and parameters
- **Outputs**: Files or directories generated by the function
- **Execution**: How the function runs (command, environment)

### File Contract

The Agent runs your module functions by executing a command (`run_command` in the manifest). It passes three paths:

- **input file** (`input_file.json`) — JSON with model file paths and parameters
- **output file** (`output_file.json`) — your module writes a JSON array of output file metadata here
- **temp dir** — Temporary directory for intermediate files

## Step 1: Start the Docker Environment

### 1.1 Create the Dockerfile

```dockerfile
FROM ubuntu:24.04

ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update && apt-get install -y --no-install-recommends \
    zip \
    unzip \
    ca-certificates \
    jq \
    nano

WORKDIR /workspace

CMD ["sleep", "infinity"]
```

### 1.2 Create docker-compose.yml

```yaml
services:
  istari-tutorial:
    build: .
    image: istari-tutorial:24.04
    platform: linux/amd64
    container_name: istari-tutorial
    volumes:
      - .:/workspace
      - ./downloads:/downloads:ro
    working_dir: /workspace
    stdin_open: true
    tty: true
```

> **Note:** `platform: linux/amd64` is required because the Istari Digital CLI and Agent are only available for amd64. On Apple Silicon Macs this runs under Docker emulation.

### 1.3 Place downloads

Create a `downloads` folder in the same path as your Docker files. Download the Istari CLI and agent packages from the Customer Portal and place the files in the newly-created `downloads` folder.

Linux/MacOS terminal example:

```bash
mkdir -p downloads
# cp ~/Downloads/stari_ubuntu_24_04-amd64-v<VERSION>.tar.gz downloads/
# cp ~/Downloads/istari-agent_X.Y.Z_amd64.deb downloads/
```

In this tutorial we are using

- Istari Digital CLI - Debian AMD64 Ubuntu 24.04 `tar.gz` package
- Istari Agent - Debian AMD64 (non-FIPS) `.deb` package

### 1.4 Start the container and open a shell

```bash
docker compose up -d --build
docker compose exec istari-tutorial bash
```

You are now in a bash shell inside Ubuntu 24.04 (amd64). The project root is mounted at `/workspace`, and `downloads/` is at `/downloads` (read-only).

All remaining steps are run **inside this container**.

> **Editing files:** The Dockerfile includes `nano`, so you can edit files directly inside the container (e.g. `nano module_manifest.json`). You can also use your preferred editor on the host — because `/workspace` is a mounted volume, any changes you make on the host are immediately visible inside the container.

## Step 2: Install and Initialize the CLI

### 2.1 Install the CLI

Extract and set up the CLI from `/downloads`:

```bash
cd /tmp
tar -xzf /downloads/stari_ubuntu_24_04-amd64-v<VERSION>.tar.gz
cd stari_ubuntu_24_04-amd64
./stari_ubuntu_24_04-amd64 path add --yes
source ~/.bashrc
stari --version
```

`path add` puts `stari` on your PATH. Run it as the account inside the container, not with `sudo`. See [Get started with the CLI](../../icbs-install/05-CLI/01-get-started.md).

### 2.2 Download a CLI key

The CLI signs in with a key file. Download the file whole. It is JSON with `clientId`, `keyId`, and `key`. Use the file as-is. Copying one of those fields out of it is not a credential the CLI can use.

Personal Access Tokens are deprecated as of the July 2026 release. This tutorial uses keys. The Agent gets its own key later, in [Step 6](#62-give-the-agent-its-credentials), the same way as [Install the agent on Linux](../../icbs-install/04-Agents/03-Install-on-Linux.md).

1. Open the Istari Digital web app.
2. Click your avatar at the bottom of the sidebar. The hover label is **User settings**.
3. Select **Developer**. The page title is **Developer Settings**.
4. Click **Generate Key**. In the **New Access Key** dialog, enter a name if you want one, choose an expiration, and click **Generate**. See [Generate a Key](../../users/user-guide/settings.md#generate-a-key).
5. Click **Download credentials** while the key is on screen. The browser saves `istari-credentials-<keyId>.json`. It is shown once.
6. Copy that file into the project folder on the host, next to `docker-compose.yml`, so the container can read it at `/workspace/`.

On that **Developer Settings** page, under **Endpoints**, copy the **API URL** (for example `https://api.your-instance.istari.app`). That is the address the CLI and the Agent call. The address in the browser, where you sign in, is the web app (for example `https://your-instance.istari.app`). They are different hosts.

Create `.env` in the project folder. These names are only for the commands in this tutorial. The CLI does not read `.env`.

```bash
API_URL=https://api.your-instance.istari.app
WEB_APP_URL=https://your-instance.istari.app
CLI_CREDENTIALS_FILE=/workspace/istari-credentials.json
```

Inside the container:

```bash
cd /workspace
source /workspace/.env
```

### 2.3 Initialize the CLI

```bash
stari client init "$API_URL" --identity-service --credentials-file "$CLI_CREDENTIALS_FILE" --yes
stari client ping
```

This is the same command as [Get started with the CLI](../../icbs-install/05-CLI/01-get-started.md#3-authenticate-with-a-user-key). On CLI 0.21.20, `--identity-service` and `--credentials-file` belong on `stari client init`.

You should see a successful connection.

```text
Successfully connected
Successfully connected to Istari Digital at <api_url>.
```

`stari client init` writes a `cli:` section into `/root/.config/istari_digital/istari_digital_config.yaml`. The Agent uses the same file. Keep that `cli:` section when you add the Agent configuration in [Step 6.2](#62-give-the-agent-its-credentials).

### 2.4 Add the `default` section once

The Agent reads a `default:` section. `stari client init` leaves it out, and startup then fails with `KeyError: 'default'`. Add the section when it is missing. Appending it again creates a duplicate key.

```bash
CONFIG=/root/.config/istari_digital/istari_digital_config.yaml
grep -q '^default:' "$CONFIG" || printf 'default: {}\n' >> "$CONFIG"
```

The top of the file should contain:

```yaml
default: {}
```

## Step 3: Create the Module

No scaffolding needed. Create the module directory with just two files: a manifest and a shell script.

```bash
mkdir -p /workspace/my-module
cd /workspace/my-module
```

### 3.1 Create the shell script

Create `copy.sh`:

```bash
#!/usr/bin/env bash
# Istari module: copies input file to output with a timestamp header.
# Usage: copy.sh --input-file <path> --output-file <path> --temp-dir <path>
set -e

# Parse arguments
while [[ $# -gt 0 ]]; do
  case "$1" in
    --input-file)  INPUT_FILE="$2";  shift 2 ;;
    --output-file) OUTPUT_FILE="$2"; shift 2 ;;
    --temp-dir)    TEMP_DIR="$2";    shift 2 ;;
    *) shift ;;
  esac
done

# Read the input file path from the input JSON
FILE_PATH=$(jq -r '.input_file.value' "$INPUT_FILE")

# Copy with timestamp header
OUTPUT_PATH="${TEMP_DIR}/copied_file.txt"
echo "# Processed at $(date -Iseconds)" > "$OUTPUT_PATH"
echo "" >> "$OUTPUT_PATH"
cat "$FILE_PATH" >> "$OUTPUT_PATH"

# Write the output JSON. The file field is path, not value.
printf '[{"name": "copied_file", "type": "file", "path": "%s"}]\n' "$OUTPUT_PATH" > "$OUTPUT_FILE"
```

The Agent reads `path` from each output item. A field named `value`, or a `path` that is empty, makes the later upload fail.

Make it executable:

```bash
chmod +x copy.sh
```

### 3.2 Create the manifest

Create `module_manifest.json` (replace `<namespace>`):

```json
{
  "module_key": "@<namespace>:agent-301",
  "module_version": "1.0.0",
  "module_checksum": "placeholder",
  "module_type": "function",
  "module_display_name": "301 Tutorial",
  "tool_key": "<namespace>-tool",
  "tool_display_name": "<Namespace> Tool",
  "tool_versions": ["1.0.0"],
  "operating_systems": ["Ubuntu 24.04"],
  "agent_version": ">=9.0.0",
  "internal": false,
  "authors": [
    {
      "name": "Your Name",
      "email": "you@example.com"
    }
  ],
  "functions": {
    "@<namespace>:copy": [
      {
        "entrypoint": "copy.sh",
        "run_command": "bash {entrypoint} --input-file {input_file} --output-file {output_file} --temp-dir {temp_dir}",
        "operating_systems": ["Ubuntu 24.04"],
        "tool_versions": ["1.0.0"],
        "function_schema": {
          "inputs": {
            "input_file": {
              "type": "user_model",
              "validation_types": ["@extension:txt"],
              "optional": false,
              "display_name": "Input File"
            }
          },
          "outputs": [
            {
              "name": "copied_file",
              "type": "file",
              "required": true,
              "upload_as": "artifact",
              "display_name": "Copied File"
            }
          ]
        }
      }
    ]
  }
}
```

Key points:

- **`module_key`**: `@<namespace>:agent-301` — unique per developer on the control plane.
- **`tool_key`**: `<namespace>-tool` — a unique identifier for the tool.
- **`tool_display_name`**: `<Namespace> Tool` — the human-readable name shown in the Istari Digital web app when granting tool access.
- The function key `@<namespace>:copy` **must use the same namespace** as `module_key`.
- `run_command` calls `bash {entrypoint} ...` — the Agent resolves `{entrypoint}` to the path of `copy.sh`.
- The shell script parses `--input-file`, `--output-file`, `--temp-dir` like any CLI tool.
- We are using a simple shell script but the same approach works with any executable, including a compiled binary.

### Validate the manifest

```bash
stari module lint module_manifest.json
```

## Step 4: Test Locally

```bash
cd /workspace/my-module
mkdir -p test_run
echo "Hello, Istari!" > test_run/input.txt
```

Create `test_run/input.json`:

```json
{
  "input_file": {
    "type": "user_model",
    "value": "test_run/input.txt"
  }
}
```

Run the script:

```bash
bash copy.sh \
  --input-file test_run/input.json \
  --output-file test_run/output.json \
  --temp-dir test_run
```

Check the results before you publish. A script can exit 0 and still write an output JSON the Agent will reject.

```bash
cat test_run/output.json
jq . test_run/output.json
OUTPUT_PATH=$(jq -r '.[0].path' test_run/output.json)
test -n "$OUTPUT_PATH" && test -s "$OUTPUT_PATH" && echo "output file exists: $OUTPUT_PATH"
cat test_run/copied_file.txt
```

`output.json` should look like this, with a real path in `path`:

```json
[{ "name": "copied_file", "type": "file", "path": "test_run/copied_file.txt" }]
```

The copied file should start with a timestamp header.

## Step 5: Package and Publish

Publishing and installing are two steps.

- **Publish** sends `module_manifest.json` to the platform, so the function shows up when you create a job.
- **Package** builds the zip you unpack onto the Agent in [Step 6](#63-install-the-module-on-the-agent). Publishing does not copy `copy.sh` onto the Agent.

### 5.1 Package

```bash
cd /workspace/my-module
zip -r /workspace/my-module.zip . -x "test_run/*"
```

### 5.2 Publish to the Platform Registry

```bash
stari client publish module_manifest.json
```

### 5.3 Provide access

The user who publishes the module has access by default. To give access to other users:

1. Open **Admin Settings** (click your name at the bottom of the left nav bar).
2. Select **User** → select a user → open the ellipsis menu (⋯).
3. Select **Manage Tool Access**.
4. Check the tool that matches your **`tool_display_name`** (e.g. `Alice Tool`).

## Step 6: Set Up the Agent

Follow [Install the agent on Linux](../../icbs-install/04-Agents/03-Install-on-Linux.md). The container is the host. It runs as root, so the commands below omit `sudo`. The Agent and the module must be installed in this same container.

### 6.1 Install the agent

On Ubuntu, install the prerequisite, then the package. Use the non-FIPS `.deb`.

```bash
apt-get update && apt-get install -y libmpv1
dpkg -i /downloads/istari-agent_X.Y.Z_amd64.deb
```

The agent installs to `/opt/local/istari_agent/`.

### 6.2 Give the agent its credentials

This is the same sequence as step 3 of the Linux guide. The CLI is not part of it. `stari client init` already wrote the `cli:` section and does not configure the Agent.

1. In the web app, open **Agents** and select **Generate Key**. Select **Download credentials** while the key is on screen. The browser saves `agent-credentials-<key id>.json`. It is shown once. Generating the key needs an administrator of the tenant.
2. Copy that file into the project folder on the host, next to `docker-compose.yml`.
3. Inside the container, move it into root's home and close it to everyone else. Root is the account that will run the agent:

```bash
mkdir -p ~/.istari_digital
mv /workspace/agent-credentials-<key id>.json ~/.istari_digital/
chmod 600 ~/.istari_digital/agent-credentials-<key id>.json
```

4. Open `/root/.config/istari_digital/istari_digital_config.yaml` (`nano` is in the image). It already has `default:` and `cli:` from [Step 2](#23-initialize-the-cli). Add an `agent:` section and leave `cli:` in place.

The container has no desktop session, so include headless mode, as the Linux guide does before you start the agent:

```yaml
agent:
  istari_digital_agent_digital_api_url: "https://api.example.istari.app"
  istari_digital_agent_identity_service_secret_file: "/root/.istari_digital/agent-credentials-<key id>.json"
  istari_digital_agent_identity_service_enabled: true
  istari_digital_agent_headless_mode: true
```

Put the API URL from [Step 2.2](#22-download-a-cli-key) in `istari_digital_agent_digital_api_url`. The configuration stores the path to the credentials file. The agent reads that file on every start.

### 6.3 Install the module on the agent

The Agent only runs a module installed on the same machine. In this tutorial that machine is the container.

```bash
mkdir -p /opt/local/istari_agent/istari_modules/my-module
cp /workspace/my-module.zip /opt/local/istari_agent/istari_modules/my-module/
unzip -o /opt/local/istari_agent/istari_modules/my-module/my-module.zip -d /opt/local/istari_agent/istari_modules/my-module
ls /opt/local/istari_agent/istari_modules/my-module/module_manifest.json \
   /opt/local/istari_agent/istari_modules/my-module/copy.sh
```

Both files must be listed. If either path is missing, the Agent cannot run the function.

### 6.4 Start the Agent

```bash
/opt/local/istari_agent/istari_agent_X.Y.Z
```

**Expected result (successful start):**

```text
 - MainThread - INFO - Agent initialized
 - MainThread - INFO - Istari agent version X.Y.Z
 - MainThread - INFO - Configuration: istari_digital_agent_poll_interval=15
 ...
 - MainThread - INFO - Agent ID: 063d23f7-49aa-4386-9f58-260d2ed7cb58
 - registration_thread - INFO - Got display name 'trusty-oin-4579' from server
 - MainThread - INFO - No compatible jobs
 - MainThread - INFO - Checking for available jobs
```

`Got display name` is the name the platform assigned. The same name appears under **All Agents** in the web app. That is how [Install the agent on Linux](../../icbs-install/04-Agents/03-Install-on-Linux.md#5-confirm-it-registered) confirms the agent registered.

`No compatible jobs` and `Checking for available jobs` repeat about every 15 seconds. That is the Agent waiting for work. Leave this terminal running.

A compatibility warning at startup still matters when the Agent registers and polls. Check that the CLI, the Agent, and the platform release are a supported set before you submit a job. See [Troubleshooting](#troubleshooting).

Step 7 needs a second shell. On the host, in the project folder:

```bash
docker compose exec istari-tutorial bash
```

## Step 7: Run the function in the web app

Sign in at `WEB_APP_URL` from your `.env` (for example `https://your-instance.istari.app`). That host is the web app. `API_URL` (`https://api...`) is only for the CLI and the Agent.

On your computer, make a small text file to upload:

```bash
printf 'Hello, Istari!\n' > input.txt
```

In the web app:

1. Open **Resources**.
2. Select **Add resources** → **Upload**, and choose `input.txt`. See [Add Resources](../../users/user-guide/add-resources.md).
3. Open the Resource and select **Create Job**.
4. In **Tool/Function Combination**, select the tool and function you published (`<Namespace> Tool` / `@<namespace>:copy`).
5. Select **Execute Function**.

Watch the job under **Jobs**. Details: [Jobs and Automation](../../users/user-guide/jobs.md).

In the terminal where the Agent is running, you should see entries like:

```bash
Found available job 6b1d8333-d0c8-4d9b-992e-975724a5e8c6
CLAIMING JOB STATE: job 6b1d8333...
EXECUTING JOB STATE: job 6b1d8333...
Running process 6b1d8333... in cwd /opt/local/istari_agent/istari_modules/my-module with command: ['bash', '/opt/local/istari_agent/istari_modules/my-module/copy.sh', '--input-file', '...input.json', '--output-file', '...output.json', '--temp-dir', '...out']
Process 6b1d8333... completed succesfully
Return code for job 6b1d8333...: 0
Job 6b1d8333... completed successfully
EXECUTION SUCCESS STATE: job 6b1d8333...
```

### Check results

In the web app, open **Resources** and open the Resource you uploaded. The job adds the copied file as an artifact on that Resource. The run itself is listed under **Jobs**.

---

## Cleaning Up

When you are done, exit the container and stop it:

```bash
exit
docker compose down
```

---

## Summary: What You've Achieved

By completing this tutorial, you've built and deployed your first **Platform module** with a `@<namespace>:copy` function that:

- **Accepts a text file** as input,
- **Adds a timestamp header** to the content,
- **Returns the processed file** as an artifact,
- **Runs as a job** on the Istari Digital Platform.

### Key Skills You've Learned

- **Module Structure:** How to create a module with just a script and a manifest.
- **Function Design:** Defining inputs, outputs, and execution logic.
- **Local Testing:** Validating your function before deployment.
- **Deployment Workflow:** Packaging, publishing, and granting access to your module.
- **Agent Integration:** Setting up and running the Agent to execute jobs.

---

## Troubleshooting

### `DisplayNameError: Bad display name ""`

The Agent tries to start a system-tray icon, which requires a graphical display. In Docker or over SSH (headless), there is no display and the Agent crashes.

**Fix:** Enable headless mode in your Agent config. Add the following under `agent:` in `istari_digital_config.yaml`:

```yaml
agent:
  istari_digital_agent_headless_mode: true
```

### `KeyError: 'default'`

The CLI doesn't write a `default` section but the Agent expects it.

**Fix:** Add `default: {}` only when it is missing. See [Step 2.4](#24-add-the-default-section-once).

### Output JSON has an empty `path`

The Agent uploads the file named in `path`. This value fails that upload:

```json
[{ "name": "copied_file", "type": "file", "path": "" }]
```

**Fix:** In `copy.sh`, set `OUTPUT_PATH` to the file you wrote, and pass that variable to `printf`. Re-run the checks in [Step 4](#step-4-test-locally) before you package again.

### Output JSON uses `value` instead of `path`

The Agent validates each output with `OutputItem.path`.

**Fix:**

```json
[{ "name": "copied_file", "type": "file", "path": "/tmp/copied_file.txt" }]
```

### Compatibility warning at startup

A warning that the Agent or CLI does not match the registry API means the versions are not a supported set. The Agent can still register and poll. Treat the warning as something to fix: use the CLI and Agent versions for your platform release. This tutorial was checked with CLI 0.21.20 and Agent 11.11.2.

### `Module version 1.0.0 already exists`

```
HTTP response body: {"detail":"Malformed Manifest: \"Module version 1.0.0 already exists.\""}
```

The registry rejects a publish if that exact version has already been uploaded. Module versions are immutable once published.

**Fix:** Bump `module_version` in `module_manifest.json` (e.g. `1.0.0` → `1.0.1`) and publish again.


---

<!-- Source: extend/302-build-module-python.md -->

---
sidebar_position: 2
sidebar_label: "302 - Python module"
title: "Integration 302: Python"
---

# Integration 302: Build a Python Module

## Objective

Create a module whose function is implemented in **Python** and compiled into a **standalone binary** with [PyInstaller](https://pyinstaller.org). The binary runs without a Python installation on the target machine.

This tutorial builds on [Integration 301](./301-build-module.md). You will reuse the same Docker-based workflow, credentials, and Agent setup — so those steps are referenced rather than repeated.

By the end you will know how to:

- Scaffold a Python module project with the Istari Digital CLI
- Implement a function using the scaffold's framework (Pydantic models, function registry)
- Build the module into a self-contained executable
- Test, package, publish, and run it on the platform

> **Namespace:** As in Integration 301, replace `<namespace>` with your own unique value (e.g. `alice`, `teamx`).

## Prerequisites

- You have completed [Integration 301](./301-build-module.md).
- [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/) installed.
- The **Istari Digital CLI** and **Agent installer** in a `downloads/` directory (same packages as Integration 301). Replace `<VERSION>` with the CLI version and `X.Y.Z` with the Agent version. This tutorial was checked with CLI **0.21.20** and Agent **11.11.2**.

## Step 1: Start the Python Development Container

The Docker image for this tutorial includes Python, Poetry, the Istari Digital CLI, and the Agent — all pre-installed. Create these files in a **new project directory** (e.g. `python-module-tutorial/`).

### 1.1 Place downloads

```bash
mkdir -p downloads
# cp ~/Downloads/stari_ubuntu_24_04-amd64-v<VERSION>.tar.gz downloads/
# cp ~/Downloads/istari-agent_X.Y.Z_amd64.deb downloads/
```

Put one CLI archive and the non-FIPS Agent `.deb` in `downloads/`. The Dockerfile copies whichever file matches.

### 1.2 Create the Dockerfile

```dockerfile
FROM ubuntu:24.04

ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update && apt-get install -y --no-install-recommends \
    python3 \
    python3-pip \
    python3-venv \
    zip \
    unzip \
    ca-certificates \
    jq \
    nano \
    binutils \
    libpython3.12 \
    libmpv1

RUN pip install --break-system-packages "pydantic>=2" poetry

COPY downloads/stari_ubuntu_24_04-amd64-v*.tar.gz /tmp/stari.tar.gz
RUN cd /tmp && tar -xzf stari.tar.gz \
    && cd stari_ubuntu_24_04-amd64 \
    && ./stari_ubuntu_24_04-amd64 path add --yes \
    && rm -rf /tmp/stari*
ENV PATH="/root/.istari_digital/bin:${PATH}"

COPY downloads/istari-agent_*_amd64.deb /tmp/agent.deb
RUN dpkg -i /tmp/agent.deb && rm /tmp/agent.deb

WORKDIR /workspace

CMD ["sleep", "infinity"]
```

Compared with Integration 301 this image adds Python development packages and **pre-installs the CLI and Agent**, so they are ready when the container starts.

### 1.3 Create docker-compose.yml

```yaml
services:
  istari-tutorial:
    build: .
    image: istari-python-tutorial:24.04
    platform: linux/amd64
    container_name: istari-python-tutorial
    volumes:
      - .:/workspace
    working_dir: /workspace
    stdin_open: true
    tty: true
```

### 1.4 Create the `.env` file

Copy `.env` and `istari-credentials.json` from Integration 301 into this project folder, or create them the same way. See [Integration 301 — Step 2.2](./301-build-module.md#22-download-a-cli-key).

```bash
API_URL=https://api.your-instance.istari.app
WEB_APP_URL=https://your-instance.istari.app
CLI_CREDENTIALS_FILE=/workspace/istari-credentials.json
```

These names are only for the commands in this tutorial. The CLI does not read `.env`. The Agent key is created in [Step 8](#step-8-deploy-and-run), the same way as [Install the agent on Linux](../../icbs-install/04-Agents/03-Install-on-Linux.md).

### 1.5 Start the container and initialise

```bash
docker compose up -d --build
docker compose exec istari-tutorial bash
```

Inside the container, initialise the CLI with the key file:

```bash
source /workspace/.env
stari client init "$API_URL" --identity-service --credentials-file "$CLI_CREDENTIALS_FILE" --yes
stari client ping
```

`stari client init` writes a `cli:` section into `/root/.config/istari_digital/istari_digital_config.yaml`. Leave that section in place when you configure the Agent in [Step 8](#step-8-deploy-and-run).

The Agent reads a `default:` section. Add it when it is missing:

```bash
CONFIG=/root/.config/istari_digital/istari_digital_config.yaml
grep -q '^default:' "$CONFIG" || printf 'default: {}\n' >> "$CONFIG"
```

## Step 2: Scaffold the Module

The CLI generates a complete Python module project:

```bash
cd /workspace
stari module scaffold my-python-module \
  --type python-module \
  --author "Your Name" \
  --email "you@example.com" \
  --description "Tutorial: Python compiled module" \
  --version "1.0.0"
```

This creates the following structure:

```
my-python-module/
├── module_manifest.json
├── pyproject.toml
├── module/
│   ├── __init__.py
│   ├── __main__.py            ← entry point (arg parsing, function dispatch)
│   ├── logging_config.py
│   ├── module_config.py
│   └── functions/
│       ├── __init__.py         ← auto-discovers function files at import time
│       ├── registry.py         ← register() / get_function()
│       ├── base/
│       │   └── function_io.py  ← Input, Output, OutputType types
│       ├── data_extraction.py
│       ├── model_to_artifact_no_auth.py   ← simplest template (we will use this one)
│       ├── model_to_artifacts_no_auth.py
│       ├── model_to_artifacts_basic_auth.py
│       ├── model_to_artifacts_oidc_auth.py
│       └── model_params_to_artifacts_no_auth.py
├── tests/
├── scripts/
├── hooks/
└── README.md
```

The scaffold ships with several **template functions** covering common patterns. Each one is a working example you can run immediately. Open the project's `README.md` to see the full table:

| Use case                                | Template file                          |
| --------------------------------------- | -------------------------------------- |
| 1 input → 1 output, no auth             | `model_to_artifact_no_auth.py`         |
| 1 input → multiple outputs              | `model_to_artifacts_no_auth.py`        |
| 1 input + parameters → multiple outputs | `model_params_to_artifacts_no_auth.py` |
| Basic auth (username/password)          | `model_to_artifacts_basic_auth.py`     |
| OIDC/JWT auth                           | `model_to_artifacts_oidc_auth.py`      |

### Install dependencies

```bash
cd my-python-module
poetry install
```

Verify the scaffold compiles and the existing templates' tests pass:

```bash
poetry run pytest
```

## Step 3: Create Your Function from a Template

The recommended workflow is to **copy the closest template** and modify it. Our function takes one input file and produces one output file with no authentication, so `model_to_artifact_no_auth.py` is the right starting point.

### 3.1 Copy the template

```bash
cp module/functions/model_to_artifact_no_auth.py module/functions/file_copy.py
```

### 3.2 Understand the template structure

Open `module/functions/file_copy.py`. The template has two clearly separated sections:

```python
# ============================================================================
# YOUR BUSINESS LOGIC - REPLACE THIS SECTION
# ============================================================================
def process_model(input_path: Path, output_path: Path) -> None:
    """Replace this with your actual processing logic."""
    content = input_path.read_text(encoding="utf-8")
    output_path.write_text(f"Processed: {content}", encoding="utf-8")
# ============================================================================
# END OF YOUR BUSINESS LOGIC
# ============================================================================

# --- Framework code below (usually no changes needed) ---
```

The **business logic** section is the only part you must change. The framework code below it handles input parsing, error handling, and output metadata — you typically leave it as-is, though you will rename a few things.

### 3.3 Edit the file

Make the following changes to `module/functions/file_copy.py`:

**1. Replace the business logic** — add a timestamp header when copying:

```python
def process_model(input_path: Path, output_path: Path) -> None:
    """Copies the input file and prepends a timestamp header."""
    from datetime import datetime

    content = input_path.read_text(encoding="utf-8")
    header = f"# Processed at {datetime.now().isoformat()}\n\n"
    output_path.write_text(header + content, encoding="utf-8")
```

**2. Rename the Pydantic input class** from `ModelToArtifactNoAuthInput` to `FileCopyInput`:

```python
class FileCopyInput(BaseModel):
    """Input schema for the function."""
    ...
```

**3. Rename the framework function and update its internals** — change the function name, use `FileCopyInput`, and derive the output name from the input filename:

```python
def file_copy(input_json: str, temp_dir: str) -> List[Output]:
    ...
    function_input = FileCopyInput.model_validate_json(input_json)
    ...
    output_path = Path(temp_dir) / f"copy_{input_path.name}"
    ...
```

**4. Update the `register()` call** at the bottom:

```python
register("FileCopy", file_copy)
```

### 3.4 Final file

After all edits, `module/functions/file_copy.py` should look like this:

```python
"""
File copy function — copies input file to output with a timestamp header.
Based on the ModelToArtifactNoAuth template.
"""

import logging
from datetime import datetime
from pathlib import Path
from typing import List

from pydantic import BaseModel, ConfigDict, Extra, Field, ValidationError

from module.functions.base.function_io import Input, Output, OutputType
from module.functions.registry import register

logger = logging.getLogger(__name__)


# ============================================================================
# YOUR BUSINESS LOGIC
# ============================================================================
def process_model(input_path: Path, output_path: Path) -> None:
    """Copies the input file and prepends a timestamp header."""
    content = input_path.read_text(encoding="utf-8")
    header = f"# Processed at {datetime.now().isoformat()}\n\n"
    output_path.write_text(header + content, encoding="utf-8")


# ============================================================================
# END OF YOUR BUSINESS LOGIC
# ============================================================================


class FileCopyInput(BaseModel):
    """Input schema for the function."""

    input_model: Input[str] = Field(
        ...,
        description="The input model file to process.",
    )

    model_config = ConfigDict(extra=Extra.allow)


def file_copy(input_json: str, temp_dir: str) -> List[Output]:
    """
    Processes a single model file and produces a single artifact.
    """
    logger.info("Starting FileCopy execution.")

    try:
        function_input = FileCopyInput.model_validate_json(input_json)
    except ValidationError as e:
        raise ValueError(f"Invalid input JSON for FileCopy: {e}") from e

    outputs: List[Output] = []

    input_path = Path(function_input.input_model.value)
    output_path = Path(temp_dir) / f"copy_{input_path.name}"

    try:
        process_model(input_path, output_path)
    except OSError:
        logger.exception(f'Failed to process model at "{input_path}".')
        return outputs

    outputs.append(
        Output(
            name="output_artifact",
            type=OutputType.FILE,
            path=str(output_path),
        ),
    )

    return outputs


register("FileCopy", file_copy)
```

### What changed compared to the template

| What            | Template (`model_to_artifact_no_auth.py`) | Our function (`file_copy.py`)   |
| --------------- | ----------------------------------------- | ------------------------------- |
| Business logic  | Prepends `"Processed: "`                  | Prepends a timestamp header     |
| Input class     | `ModelToArtifactNoAuthInput`              | `FileCopyInput`                 |
| Function name   | `model_to_artifact_no_auth`               | `file_copy`                     |
| Output name     | `"output_artifact"`                       | `"output_artifact"` (unchanged) |
| Registered name | `"ModelToArtifactNoAuth"`                 | `"FileCopy"`                    |

The framework code (argument parsing, error handling, output writing) is unchanged — that is the point of using a template.

### 3.5 Update `__init__.py` to import only your function

By default, `module/functions/__init__.py` auto-discovers and imports every `.py` file in the directory. That means all the scaffold templates would be registered alongside your function. Replace the auto-discovery with an explicit import of your function only:

Edit `module/functions/__init__.py`:

```python
"""Istari functions offered by the module."""

import module.functions.file_copy
```

This keeps the project clean — only `FileCopy` is registered and available to the Agent.

## Step 4: Update the Manifest

Replace the contents of `module_manifest.json` (replace `<namespace>`):

```json
{
  "module_key": "@<namespace>:python-tutorial",
  "module_version": "1.0.0",
  "module_checksum": "placeholder",
  "module_type": "function",
  "module_display_name": "Python Tutorial Module",
  "tool_key": "<namespace>-tool",
  "tool_display_name": "<Namespace> Tool",
  "tool_versions": ["1.0.0"],
  "operating_systems": ["Ubuntu 24.04"],
  "agent_version": ">=9.0.0",
  "internal": false,
  "authors": [
    {
      "name": "Your Name",
      "email": "you@example.com"
    }
  ],
  "functions": {
    "@<namespace>:copy": [
      {
        "entrypoint": "python_module",
        "run_command": "{entrypoint} FileCopy --input-file {input_file} --output-file {output_file} --temp-dir {temp_dir}",
        "operating_systems": ["Ubuntu 24.04"],
        "tool_versions": ["1.0.0"],
        "function_schema": {
          "inputs": {
            "input_model": {
              "type": "user_model",
              "validation_types": ["@extension:txt"],
              "optional": false,
              "display_name": "Input File"
            }
          },
          "outputs": [
            {
              "name": "output_artifact",
              "type": "file",
              "required": true,
              "upload_as": "artifact",
              "display_name": "Copied File"
            }
          ]
        }
      }
    ]
  }
}
```

Key differences from Integration 301:

- **`entrypoint`** points to `python_module` — the compiled binary that PyInstaller produces (placed at the zip root alongside the manifest).
- **`run_command`** passes `FileCopy` as the first argument — this is the registered function name (the one in `register()`), not the Python file name.
- The `inputs` key `input_model` must match the field name in the Pydantic model (`FileCopyInput.input_model`).
- The `outputs` name `output_artifact` must match the `name` in the `Output` object returned by the function.

Validate the manifest:

```bash
stari module lint module_manifest.json
```

:::tip
JSON schemas for validation and code assistants

The CLI ships with JSON schemas that it uses for validation. You can export them for use in your editor or with LLM code assistants:

```bash
stari schemas get module_manifest
stari schemas get function_schema
stari schemas get function_input
stari schemas get function_output
```

Each command saves the schema to a `.json` file in the current directory. Providing these schemas as context to an AI coding assistant helps it generate valid manifests and function I/O structures.
:::

## Step 5: Test Locally

### 5.1 Create test files

```bash
mkdir -p test_run
echo "Hello from the Python module!" > test_run/input.txt
```

Create `test_run/input.json`:

```json
{
  "input_model": {
    "type": "user_model",
    "value": "test_run/input.txt"
  }
}
```

### 5.2 Run with Poetry (pre-build)

Before compiling, test directly with Python:

```bash
poetry run python -m module FileCopy \
  --input-file test_run/input.json \
  --output-file test_run/output.json \
  --temp-dir test_run
```

### 5.3 Verify output

```bash
cat test_run/output.json
cat test_run/copy_input.txt
```

Expected output:

```
# Processed at 2026-02-25T14:00:00.000000

Hello from the Python module!
```

## Step 6: Build the Binary

PyInstaller bundles Python, your code, and all dependencies into a single executable. The scaffold includes a pre-configured build task:

```bash
poetry run poe build_binary
```

This runs `pyinstaller --onefile --additional-hooks-dir=./hooks --name=python_module module/__main__.py` and produces `dist/python_module`.

### Test the compiled binary

```bash
rm test_run/output.json test_run/copy_input.txt

./dist/python_module FileCopy \
  --input-file test_run/input.json \
  --output-file test_run/output.json \
  --temp-dir test_run

cat test_run/copy_input.txt
```

The binary should produce the same result as the Poetry run.

## Step 7: Package and Publish

### 7.1 Package

The Agent only needs the compiled binary and the manifest. Zip them together without the `dist/` folder level:

```bash
cd /workspace/my-python-module
zip /workspace/my-python-module.zip module_manifest.json
cd dist && zip /workspace/my-python-module.zip python_module && cd ..
```

### 7.2 Publish

```bash
stari client publish module_manifest.json
```

### 7.3 Grant access

Same procedure as [Integration 301 — Step 5.3](./301-build-module.md#53-provide-access): open **Admin Settings** → **User** → **Manage Tool Access** and enable your tool.

## Step 8: Deploy and Run

The Agent package is already installed in the image. Give it a key the same way as [Install the agent on Linux](../../icbs-install/04-Agents/03-Install-on-Linux.md#3-give-the-agent-its-credentials), then deploy your module. The container runs as root, so these commands omit `sudo`.

1. In the web app, open **Agents** and select **Generate Key**. Select **Download credentials** while the key is on screen. The browser saves `agent-credentials-<key id>.json`. It is shown once.
2. Copy that file into this project folder.
3. Inside the container, move it into root's home and close it to everyone else:

```bash
mkdir -p ~/.istari_digital
mv /workspace/agent-credentials-<key id>.json ~/.istari_digital/
chmod 600 ~/.istari_digital/agent-credentials-<key id>.json
```

4. Open `/root/.config/istari_digital/istari_digital_config.yaml`. It already has `default:` and `cli:` from [Step 1.5](#15-start-the-container-and-initialise). Add an `agent:` section and leave `cli:` in place. The container has no desktop session, so include headless mode:

```yaml
agent:
  istari_digital_agent_digital_api_url: "https://api.example.istari.app"
  istari_digital_agent_identity_service_secret_file: "/root/.istari_digital/agent-credentials-<key id>.json"
  istari_digital_agent_identity_service_enabled: true
  istari_digital_agent_headless_mode: true
```

Use the API URL from your `.env`. The configuration stores the path to the credentials file.

Deploy the module, then start the Agent as root:

```bash
mkdir -p /opt/local/istari_agent/istari_modules/my-python-module
unzip -o /workspace/my-python-module.zip \
  -d /opt/local/istari_agent/istari_modules/my-python-module
ls /opt/local/istari_agent/istari_modules/my-python-module/module_manifest.json \
   /opt/local/istari_agent/istari_modules/my-python-module/python_module

/opt/local/istari_agent/istari_agent_X.Y.Z
```

`Got display name` in the log is the name under **All Agents**. `No compatible jobs` and `Checking for available jobs` repeat about every 15 seconds. That is the Agent waiting for work. Leave this terminal running. Open another shell with `docker compose exec istari-tutorial bash` before you continue.

### 8.1 Submit a job

In the Istari Digital web app:

- Upload a `.txt` file.
- Select the file and choose **Create Job**.
- In the **Select a tool/function** dropdown, select the tool and function you created.
- Execute the job.

This starts a **Job** that you can monitor in the **Jobs** section.

### 8.2 Check Agent logs

In the terminal where the Agent is running, you should see:

```
Found available job 6b1d8333-d0c8-4d9b-992e-975724a5e8c6
CLAIMING JOB STATE: job 6b1d8333...
EXECUTING JOB STATE: job 6b1d8333...
Running process 6b1d8333... in cwd /opt/local/istari_agent/istari_modules/my-python-module ...
Process 6b1d8333... completed succesfully
Return code for job 6b1d8333...: 0
Job 6b1d8333... completed successfully
EXECUTION SUCCESS STATE: job 6b1d8333...
```

### 8.3 Check results

In the Istari Digital web app:

1. Go to **Resources** in the sidebar.
2. Find your Resource and view the artifact generated by the job.

---

## Summary

| What you did                       | How                                                    |
| ---------------------------------- | ------------------------------------------------------ |
| Scaffolded a Python module project | `stari module scaffold --type python-module`           |
| Implemented a function             | Pydantic input model + processing logic + `register()` |
| Tested before compiling            | `poetry run python -m module FileCopy ...`             |
| Built a standalone binary          | `poetry run poe build_binary` (PyInstaller)            |
| Packaged, published, and ran       | Same workflow as Integration 301                       |

### Next steps

- **Add more functions:** Copy another template (e.g. `model_to_artifacts_no_auth.py` for multiple outputs, or `model_to_artifacts_basic_auth.py` for authenticated access), edit the business logic, update the manifest. The auto-discovery in `__init__.py` picks up new files automatically.
- **Use parameters:** See `model_params_to_artifacts_no_auth.py` for a template that accepts user-supplied parameters alongside the input file.
- **Read the scaffold reference:** The generated project includes `REFERENCE.md` and `DEVELOPMENT.md` with detailed documentation on testing, configuration, error handling, and deployment.
- **Explore the manifest spec:** See the [Module Manifest API Reference](../../connect/04-custom-modules/api-reference/01-api-reference.md) for all available fields.

---

## Troubleshooting

### `ModuleNotFoundError: No module named 'module'`

You are running Python from outside the project directory, or Poetry's virtual environment is not active.

**Fix:** Run from the `my-python-module/` directory using `poetry run`.

### PyInstaller build fails with missing `Python.h`

PyInstaller needs Python development headers to compile.

**Fix:** Install them inside the container:

```bash
apt-get install -y python3.12-dev
```

The tutorial Dockerfile already includes `libpython3.12`, but if you use a different base image you may need this.

### `Module tool X does not match manifest tool Y`

You already published a module with a given `module_key` and `tool` name, then changed the `tool` field in the manifest and tried to publish again.
The platform binds the `tool` name to the `module_key` on first publish — subsequent versions must use the same `tool` value.

**Fix:** Either revert the `tool` field to the original value, or use a new `module_key` for the renamed tool.

### `Module version 1.0.0 already exists`

Module versions are immutable once published.

**Fix:** Bump `module_version` in `module_manifest.json` (e.g. `1.0.0` → `1.0.1`) and publish again.

### Other issues

See [Integration 301 — Troubleshooting](./301-build-module.md#troubleshooting) for Agent-related errors (`DisplayNameError`, `KeyError: 'default'`).


---

<!-- Source: foundations.md -->

---
sidebar_position: 0.5
title: Foundational Concepts Course
sidebar_label: Foundational Concepts Course
# Kept reachable at /tutorials/foundations. Hidden until the course is ready to list.
sidebar_custom_props:
  hideFromSidebar: true
---

# Foundational Concepts Course

New to the Istari Digital Platform? Start here. This self-paced course explains the core concepts used across the platform before you begin a role-based tutorial track.

- **Who it's for:** Anyone who will use, administer, develop with, or support the platform. No previous Istari experience is required.
- **Format:** Short lessons and a video tour of the web app.

<div class="course-start">

[Start the course](./training/foundations/index.mdx)

</div>

## What you will learn

The **Introduction** chapter covers:

- What digital engineering is
- What the Istari Digital Platform does
- Roles on Systems and Resources
- A tour of the web app

More chapters will be added over time.

## After the course

Choose the tutorial track that matches your role. See [Get started](./index.md) for the full list and suggested order.


---

<!-- Source: index.md -->

---
sidebar_position: 0
title: Get started
sidebar_label: Get started
---

# Get started

Hands-on lessons that walk you through the Istari Digital Platform step by step. Each track matches a role: **Connect, Build and Share** for the web app, **Automate** for the Python client, **Administer** for tenant setup, and **Extend** for custom modules.

[Guided demos](./resources/guided-demos.mdx) are short, interactive product recordings you can follow at your own pace. Sample files, including the Lab Quadcopter, are under [Models](./resources/models.md).

For conceptual background before you begin, see [Key Concepts](../intro/key-concepts.md).

## Sample files

Download this archive when a training exercise needs vendor-neutral examples. It is a Courier drone: a 3D model, drawings, a spec sheet, mission notes, a range calculator, flight-test results, and a parts list, in open formats (glTF, SVG, PDF, Markdown, HTML, XML, and JSON), plus the part packages the drone is built from.

- <a href="/files/VendorNeutralSampleFiles.zip" download target="_blank" rel="noopener noreferrer">VendorNeutralSampleFiles.zip</a>

## Roles

One person may hold more than one role.

| Role                                     | What they do                                                                                                                                                                                                                                                                                                                                                                                                 |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Infra team**                           | Install the Istari Digital Platform and [create the first Platform Administrator](../icbs-install/03-control-plane/10-identity-service/05-Create-the-First-Platform-Administrator.md). The steps are in the [Install guide](../icbs-install/01-Introduction.md). For training on how to prepare, plan, monitor, and test the install, contact [support@istaridigital.com](mailto:support@istaridigital.com). |
| **Platform Administrator**               | Create tenants and grant administrator roles.                                                                                                                                                                                                                                                                                                                                                                |
| **Tenant or organization administrator** | Add users, install agents and modules, grant tool access, and set up control tags and partner sharing.                                                                                                                                                                                                                                                                                                       |
| **Engineer**                             | Create Systems, register and version Resources, run jobs, and share work with teammates and partners.                                                                                                                                                                                                                                                                                                        |

## Tutorial tracks

### Administer

Browser-based setup in the **Istari Digital web app** for administrators. Complete these before end-user tutorials when you are standing up a new environment.

| Tutorial                                                                                    | What you will do                                                                                                           |
| ------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| [Onboard your tenant](./administer/onboard-your-organization.md)                            | Set up a tenant with members and a Tenant Administrator, connect an agent, grant tool access, and verify an extraction job |
| [Configure partner sharing (Secure Connections)](./administer/configure-partner-sharing.md) | Create Sending and Receiving Connections and verify partner sync                                                           |

See also the [Administrator Guide](../users/admin-guide/index.md) for reference on every admin page.

### Connect, Build and Share

Browser-only workflows for engineers and other end users.

| Tutorial                                                                         | What you will do                                                               |
| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| [Web app 101 — Your first digital thread](./connect-build-share/platform-101.md) | Register a file, run an extraction, add a revision, and compare extracted data |

### Automate

Script the same flows from Python using `istari-digital-client`.

| Tutorial                                                                                   | What you will do                                                                           |
| ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ |
| [Python Client 201 — Your first System](./automate/201-first-system.md)                    | Run one script: a new System on the platform with a CAD model, its generator, and a README |
| [Python Client 202 — Scripting your first digital thread](./automate/202-python-client.md) | Connect from Python, register a file, run extractions, and compare results as JSON         |
| [Python Client 203 — Share with a partner](./automate/203-share-with-partner.md)           | List shareable Secure Connections, share a resource at View or Edit access, and revoke it  |

### Extend

Build and deploy custom integration modules.

| Tutorial                                                                       | What you will do                                                                    |
| ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- |
| [Integration 301 — Your first module (shell)](./extend/301-build-module.md)    | Structure a module, define inputs and outputs, and run it through a connected agent |
| [Integration 302 — Build a Python module](./extend/302-build-module-python.md) | Implement a function in Python, compile a standalone binary, and publish the module |


---

<!-- Source: resources/guided-demos.mdx -->

---
sidebar_position: 1
title: Guided demos
---

import GuidedDemoEmbed from "@site/src/components/GuidedDemoEmbed";

# Guided demos

Guided demos are short, interactive product recordings you can follow at your own pace. Each one focuses on a specific topic in the Istari Digital Platform — step through the UI, pause when you need to, and continue when you are ready.

They sit alongside the written [tutorials](../index.md) and the rest of this documentation: use a demo to see a flow in motion, then return to the docs for prerequisites, full steps, and reference detail.

Browse the collection below. Some tutorials also embed a guided demo for the same topic when one is available.

<GuidedDemoEmbed
  src="https://guides.istaridigital.com/showcase/embed/cmro2502i002ozf0j1s1wl0ah?embed_v=2&utm_source=embed"
  openUrl="https://guides.istaridigital.com/showcase/cmro2502i002ozf0j1s1wl0ah"
  title="Documentation - Guides"
/>


---

<!-- Source: resources/models.md -->

---
sidebar_position: 2
title: Models
sidebar_label: Models
---

# Models

Sample engineering files for the tutorials.

**Lab Quadcopter** is a small drone for the tutorials. The zip includes the CAD model, drawings, requirements, test data, and the Propulsion, Power, and Flight electronics parts, across more than one revision.

- <a href="/files/tutorials/LabQuadcopter.zip" download target="_blank" rel="noopener noreferrer">LabQuadcopter.zip</a>


---

<!-- Source: training/foundations/index.mdx -->

---
title: Foundational Concepts
description: A 30-minute introduction to the Istari Digital Platform with videos, interactive demos, and quizzes.
hide_title: true
hide_table_of_contents: true
pagination_prev: null
pagination_next: null
# Same course as /tutorials/training, opened on this section. Hidden from the
# Tutorials sidebar; the course menu replaces the docs sidebar items.
displayed_sidebar: tutorials
sidebar_custom_props:
  hideFromSidebar: true
---

import CourseView from "./_CourseView";

<CourseView />


---

<!-- Source: training/index.mdx -->

---
title: Foundational Concepts
description: A 30-minute introduction to the Istari Digital Platform with videos, interactive demos, and quizzes.
hide_title: true
hide_table_of_contents: true
pagination_prev: null
pagination_next: null
# Keep the page out of the Tutorials sidebar, but still mount that sidebar so the
# course menu can replace its items.
displayed_sidebar: tutorials
sidebar_custom_props:
  hideFromSidebar: true
---

import CourseView from "./foundations/_CourseView";

<CourseView />
