Publish Article to Teams Example
This page walks through a working example of a web-based publish tool built with Python and Streamlit. 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 requestsEverything 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:
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):
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:
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.hashas thepersonavalue. Using the wrong identifier will result in a400or404error.
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:
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
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
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
conducttr_app.pyrequirements.txt
requirements.txtLast updated
Was this helpful?