Publish Article to Teams Example

This page walks through a working example of a web-based publish tool built with Python and Streamlitarrow-up-right. It covers everything needed to authenticate, load simulation data, and publish articles to one or more teams and explains the design decisions behind the code so you can adapt it confidently for your own integration.

The complete source files are at the end of this page.

Prerequisites

  • Python 3.10 or later

  • A Conducttr API key (your Bearer token)

  • Two packages:

pip install streamlit requests

Everything else used in the code (io, json, os, re, time, zipfile) is part of the Python standard library.

How the app works

The app follows a linear five-step flow, each gated on the previous:

1

Authenticate

Enter your API key and connect.

2

Choose a persona

Select which organisation will author the article.

3

Choose team(s)

Select one or more recipient teams.

4

Compose the article

Enter title, body, sentiment, and draft toggle.

5

Publish

Review a summary, confirm, and send.

Authentication

All Eagle API requests require a Bearer token in the Authorization header. The token/key is unique to the simulation space and is available via your account team.

The app accepts the key in two ways, in order of preference:

Environment variable (recommended for production):

circle-info

If you're running your code in Streamlit from Github then the app will be streamlit_app.py

UI entry: If no environment variable is found, the app shows a password field. The key is stored in Streamlit's st.session_state for the duration of the browser session and never written to disk.

The make_headers() helper constructs the correct headers from the key:

If the Connect call returns a 4xx error, the app surfaces the HTTP status and prompts you to check the key before proceeding.


Fetching Personas

Personas are the non-player character entities available in the exercise. Note that they have to be published to the simulation space. The Eagle API does not return persona data as direct JSON, it returns a url to a zip file of personas. Instead it works in two steps:

Step 1 β€” GET /v1.1/eagle/personas returns a short-lived presigned URL:

Step 2 β€” Fetch that URL to download a ZIP archive containing a single .json file with all persona data.

The JSON structure inside the ZIP is a list of persona objects.

Filtering to organisations

The app filters to organisation personas only (system_info.is_organisation === true) because only organisations can have websites:

Key persona fields

All fields used for publishing are nested inside system_info:

Field
Path
Description

Unique identifier

system_info.hash

Required for publishing. Pass this as the persona field in the POST body

Display name

system_info.name

Human-readable name shown in the UI

Handle

system_info.handle

Short username, e.g. UNODA

Profile image

system_info.profile_image_url

Avatar URL

Is organisation

system_info.is_organisation

true / false

Important: The endpoint expects system_info.hash as the persona value. Using the wrong identifier will result in a 400 or 404 error.

Caching

Personas are set up before the exercise begins (before STARTEX) and are not expected to change during a running exercise. The app caches the persona list for 5 minutes using @st.cache_data(ttl=300). In practice you could increase this TTL significantly or cache indefinitely for the duration of a session since persona data will not (typically) change mid-exercise.

If you do need to force a reload (for example, after a pre- or mid-exercise configuration change), use the Refresh data button in the app footer, which calls fetch_personas.clear() to invalidate the cache immediately.

Fetching Teams

Teams are the player structures that receive published content in this example. GET /v1.1/eagle/teams returns a list directly:

The response may be a bare array or wrapped in a teams or data key β€” the function normalises all three shapes into a consistent list of { team_id, name } objects.

Team naming conventions

Raw team names follow a convention that can be cleaned up for display:

Raw name
Displayed as
Rule

T - Syndicate A - 2025/02/15

Syndicate A

Strip T - prefix and trailing date

T - Red Cell

Red Cell

Strip T - prefix

S - Session

Session

Any S - prefix becomes "Session"

Like personas, team composition is established before STARTEX. The same 5-minute cache and manual refresh approach applies.


Publishing a Message

Publishing sends a POST /v1.1/eagle/messages for each selected team. The app sends one request per team with a 500ms delay between calls to stay within rate limits.

Request payload

Field reference

Field
Type
Required
Notes

persona

string

Yes

system_info.hash from the personas endpoint

channel

string

Yes

Use "websites" for web article publishing

title

string

Yes

Article headline

subtitle

string

No

Standfirst / deck copy

body

string

Yes

HTML supported. Plain text is auto-wrapped in <p> tags

assets

array

No

Image or video objects β€” see below

sentiment

string

Yes

"positive", "neutral", or "negative"

team_id

integer

Yes

From the teams endpoint

type

string

Yes

Always "team"

isDraft

integer

Yes

0 = publish immediately, 1 = save as draft

Asset objects

To attach an image or video, include it in the assets array:

The assetUrl must be a publicly accessible URL. The current app passes an empty assets array β€” extend the compose form if you need media attachment support.

Publishing to multiple teams

The app loops through selected teams and posts one request per team:

Results are collected and displayed per team so any individual failure is immediately visible without affecting the other publishes.


Error handling

publish_to_team() catches both HTTP errors and network exceptions without raising, returning a consistent result dict:

HTTP status codes

Status
Meaning
Action

200

Success

β€”

400

Bad request

Check payload β€” likely a missing required field or wrong persona hash

401

Unauthorised

API key is missing or invalid

403

Forbidden

API key does not have permission to publish

404

Not found

Invalid endpoint, or team_id does not exist

429

Rate limited

Reduce request frequency; increase the sleep delay

500

Server error

Retry after a short wait


Source files

conducttr_app.py

requirements.txt

Last updated

Was this helpful?