Skip to contents

Technical architecture reference. The diagrams below are hand-curated Mermaid (edit them in this file); the inventory tables at the bottom are auto-generated from the live Posit Connect API by data_scripts/generate_architecture.R via the update-architecture.yml GitHub Action, so they reflect what is actually deployed and scheduled right now.

Internal (login-required) hosted versions on Posit Connect:

System overview

Everything runs on or through one EC2 instance (tl-data.teachinglab.org, 3.131.192.25), which hosts Posit Connect and a standalone plumber API managed by pm2. Three GitHub repos feed it:

Repo What it holds How it deploys
TeachingLab (this repo) R package, scheduled Connect jobs (posit_automations/), Connect plumber APIs (api/), dashboards rsconnect::deploy* to Posit Connect
ec2_tl_scripts Qualtrics → S3 survey ETL scripts Lives at /home/ubuntu/scripts on the EC2 box
tl-api Standalone plumber API (port 8000) Lives at /home/ubuntu/plumber-apis, run by pm2 (@reboot pm2 resurrect)
teachinglab.github.io Data Hub Quarto site Azure Static Web Apps (Entra ID auth) at datahub.teachinglab.org
flowchart LR
    subgraph sources["Data Sources"]
        qualtrics["Qualtrics<br/>(surveys.teachinglab.org)"]
        monday["Monday.com boards<br/>(coaching logs, rosters,<br/>project log, logistics)"]
        gsheets["Google Sheets<br/>(student work grades,<br/>attendance, trackers)"]
    end

    subgraph ec2["AWS EC2 — tl-data.teachinglab.org"]
        subgraph connect["Posit Connect"]
            sched_jobs["~30 scheduled Quarto jobs<br/>(posit_automations/)"]
            connect_apis["4 plumber APIs<br/>(api/): monday-webhook,<br/>tracker-webhook, automated_decks,<br/>tl-survey-comm-api"]
            apps["Shiny + Streamlit dashboards<br/>(dashboards/posit-connect/)"]
        end
        subgraph pm2_api["tl-api plumber — pm2, port 8000"]
            update_ep["/update — survey ETL trigger"]
            userdb_ep["/check_email_or_name — survey autofill"]
            connect_ep["/connect — force Connect re-render"]
            sheets_ep["/create_sheet_copy"]
        end
        etl_scripts["ec2_tl_scripts<br/>/home/ubuntu/scripts<br/>update_*.R"]
    end

    subgraph storage["Storage"]
        s3["AWS S3<br/>tl-surveys-27 / -26 / -25 (FY buckets)<br/>coaching-data-tl, tl-monday,<br/>survey-ids, tl-cap-usage"]
        pins["Connect pins"]
    end

    subgraph outputs["Outputs"]
        dashboards["Dashboards on Connect<br/>(tl-data.teachinglab.org/...)"]
        datahub["Data Hub<br/>datahub.teachinglab.org<br/>(Azure SWA + Entra auth)"]
        emails["Automated emails<br/>(blastula / SMTP)"]
        sheets_out["Google Sheets trackers<br/>+ Google Slides decks"]
        qualtrics_wb["Qualtrics survey updates<br/>(site/course/facilitator choices,<br/>display logic)"]
    end

    qualtrics -. "on survey completion<br/>(web service in survey flow)" .-> update_ep
    update_ep -. "sources" .-> etl_scripts
    etl_scripts -. "clean parquet" .-> s3
    qualtrics -. "embedded JS on survey open" .-> userdb_ep
    userdb_ep -. "reads user_database.parquet" .-> s3

    monday -. "webhooks on board change" .-> connect_apis
    monday -- "API pulls" --> sched_jobs
    qualtrics -- "API pulls" --> sched_jobs
    gsheets --> sched_jobs

    sched_jobs --> s3
    sched_jobs --> pins
    sched_jobs --> emails
    sched_jobs --> sheets_out
    sched_jobs -- "writes choices back" --> qualtrics_wb

    s3 --> apps
    pins --> apps
    apps --> dashboards
    dashboards --> datahub

    classDef qualtrics fill:#0f9d8f,stroke:#0b7568,color:#ffffff
    classDef monday fill:#e2445c,stroke:#b23548,color:#ffffff
    classDef google fill:#188038,stroke:#12602a,color:#ffffff
    classDef aws fill:#ec7211,stroke:#b85a0e,color:#ffffff
    classDef connect fill:#447099,stroke:#335577,color:#ffffff
    classDef ec2svc fill:#37474f,stroke:#263238,color:#ffffff
    classDef output fill:#04abeb,stroke:#0380b0,color:#ffffff

    class qualtrics,qualtrics_wb qualtrics
    class monday monday
    class gsheets,sheets_out google
    class s3 aws
    class pins,sched_jobs,connect_apis,apps connect
    class update_ep,userdb_ep,connect_ep,sheets_ep,etl_scripts ec2svc
    class dashboards,datahub,emails output

Solid arrows = scheduled or on-load; dashed = event-driven (webhooks, survey-flow web services). Node colors: red Monday, teal Qualtrics, green Google, orange AWS, slate EC2 services, blue Posit Connect / outputs.

Event-driven flows

Three kinds of triggers exist besides the Connect schedules: Qualtrics survey flows, Monday webhooks, and Google Apps Scripts bound to sheets. The first two are diagrammed below; the third is described in the notes underneath.

flowchart TD
    subgraph survey_flow["Survey lifecycle (Qualtrics)"]
        open["Participant opens survey"] -.->|"JS calls tl-api<br/>/check_email_or_name"| autofill["Autofill site/role/demographics,<br/>skip already-answered blocks<br/>(user_database.parquet, survey-ids bucket)"]
        submit["Participant submits survey"] -.->|"web service on completion<br/>POST /update/update_survey_s3"| debounce["Debounced async run of<br/>ec2_tl_scripts/update_&lt;survey&gt;.R"]
        debounce -.-> s3w["Fresh parquet in FY bucket<br/>(tl-surveys-27)"]
    end

    subgraph monday_flow["Monday.com webhooks (Connect APIs)"]
        board_change["Coaching log / calendar<br/>board change"] -.->|webhook| mw["monday-webhook API<br/>(api/monday-webhook)"]
        mw -.-> cal["PL/coaching calendar parquet<br/>+ /calendar endpoints"]
        tracker_change["Tracker board events"] -.->|webhook| tw["tracker-webhook API<br/>(api/tracker-webhook)"]
    end

    classDef qualtrics fill:#0f9d8f,stroke:#0b7568,color:#ffffff
    classDef monday fill:#e2445c,stroke:#b23548,color:#ffffff
    classDef aws fill:#ec7211,stroke:#b85a0e,color:#ffffff
    classDef connect fill:#447099,stroke:#335577,color:#ffffff
    classDef ec2svc fill:#37474f,stroke:#263238,color:#ffffff

    class open,submit qualtrics
    class autofill,debounce ec2svc
    class s3w,cal aws
    class board_change,tracker_change monday
    class mw,tw connect

Notes:

  • run_all_files.r in ec2_tl_scripts batch-runs every non-archived update_*.R; there is no crontab entry for it on the box — survey ETL is event-driven via the plumber /update endpoint (plus manual batch runs).
  • The tl-api plumber also exposes /connect/schedule_posit_script, which lets a Qualtrics flow (or anything else) force an immediate re-render of a Connect job by GUID.
  • Scheduled Connect jobs write back to Qualtrics too: site/course/facilitator choice lists and display logic are updated daily from Monday data.
  • The tl-api plumber is reachable publicly at https://api.teachinglab.org, not only from the box — that is what lets Qualtrics survey flows and Google Apps Scripts call it. Callers authenticate with a static shared secret in an Authorization header. Google Apps Scripts are the least discoverable trigger in the whole system: they live inside individual Google Sheets, not in any repo, so nothing here shows them and grep will never find them. If a dataset refreshes at a time no Connect schedule explains, look for a bound script before anything else.

Cross-system data routing

How data moves between platforms — including the write-backs, which are the easy ones to forget. S3 is the hub: almost everything lands there as parquet before going anywhere else, and two S3 objects get read back into Qualtrics at survey runtime.

flowchart LR
    monday["Monday.com"]
    qualtrics["Qualtrics"]
    gsheets["Google Sheets / Drive"]
    gslides["Google Slides"]
    s3[("AWS S3")]
    serving["Connect dashboards,<br/>pins, emails"]

    qualtrics -. "survey responses, on submission<br/>(ec2_tl_scripts via tl-api /update)" .-> s3
    monday -- "nightly board saves<br/>(coaching log, rosters, project log,<br/>staffing, logistics, deck list)" --> s3
    monday -. "calendar parquet<br/>(monday-webhook, on board change)" .-> s3
    gsheets -- "student work grades, daily" --> s3

    monday -- "daily dropdown sync:<br/>coach / coachee / facilitator /<br/>course choices + PF flow" --> qualtrics
    gsheets -- "Master Survey Input →<br/>site choices + display logic, daily" --> qualtrics
    s3 -. "runtime lookups on survey open:<br/>user_database autofill,<br/>today's facilitators (tl-api)" .-> qualtrics

    s3 -- "coaching tracker,<br/>student outcomes, daily" --> gsheets
    monday -- "logistics subitems sync,<br/>D75 sub-school (daily + webhook)" --> monday
    monday -- "deck list → automated decks" --> gslides

    s3 --> serving
    serving -. "user-entered data:<br/>student work grades,<br/>CAP usage events" .-> s3

    yc["YC's stuff (WIP)<br/>web forms + bulk upload"] -. "coaching log form (TBD)" .-> monday
    yc -. "roster form + bulk upload (TBD)" .-> monday

    classDef qualtrics fill:#0f9d8f,stroke:#0b7568,color:#ffffff
    classDef monday fill:#e2445c,stroke:#b23548,color:#ffffff
    classDef google fill:#188038,stroke:#12602a,color:#ffffff
    classDef gslides fill:#f9ab00,stroke:#c78900,color:#20242a
    classDef aws fill:#ec7211,stroke:#b85a0e,color:#ffffff
    classDef output fill:#04abeb,stroke:#0380b0,color:#ffffff
    classDef wip fill:#8a63c9,stroke:#6b48a8,color:#ffffff,stroke-dasharray:6 4

    class qualtrics qualtrics
    class monday monday
    class gsheets google
    class gslides gslides
    class s3 aws
    class serving output
    class yc wip

Solid arrows = scheduled; dashed = event-driven or on-use (webhooks, survey runtime, user input). An interactive version of this map — hover to trace a system’s routes, click an edge for the jobs behind it — is published alongside this doc.

Route detail:

Route What moves Jobs / mechanisms Cadence
Qualtrics → S3 Survey responses → cleaned parquet in the FY bucket (tl-surveys-27) ec2_tl_scripts/update_<survey>.R, triggered by the completion web service in each survey flow (tl-api /update) Minutes after each submission
Monday → S3 Coaching log, coach roster, project log, staffing match, facilitator board, logistics lookups, deck list, misc boards → tl-monday / coaching-data-tl update_coaching_log_27, update_coaching_roster_27, update_project_log, employee_info, update_fac_board, save_various_monday_boards, update_fac_course_list_daily_fy27, update_deck_list Nightly
Monday → S3 (event) PL/coaching calendar parquet monday-webhook Connect API (+ daily rebuild) On board change
Monday → Qualtrics Survey dropdowns and logic: coach, teacher/coachee, facilitator, and course choices; participant feedback survey flow update_coach_selection, update_coachee_selection_27, update_facilitator_choices, update_course_choices, update_pf_flow Daily
Google Sheets → Qualtrics Master Survey Input workbook → site/subsite choices and display logic across surveys update_site_choices Daily
Qualtrics → S3 (files) Student work PDFs uploaded by teachers → tl-student-work-27, plus a metadata row per file in the grading sheet update_student_work Daily
Google Sheets → S3 Student work grades → parquet update_student_work Daily
S3 → Google Sheets Coaching tracker completion status; student outcomes tracker_27, update_student_outcomes(_2) Daily
S3 → Qualtrics (runtime) survey-ids/user_database.parquet (autofill + skip logic) and tl-monday/pl_today_facilitators_fy27.rds (today’s facilitator list), read when a participant opens a survey tl-api /check_email_or_name, /facilitators_coaches, called from survey-embedded JS On survey open
Monday → Monday Coaching log entries → logistics board subitems; D75 sub-school routing monday_subitems_logistics (daily), monday-webhook (event) Daily + webhook
Monday → Google Slides Deck list board → generated slide decks update_deck_list + automated_decks Connect API Daily / on demand
S3 → serving Everything above feeds Connect dashboards, pins, reports, and emails Connect apps and scheduled jobs On load / per schedule
Serving → S3 User-entered data flowing back: student work grading inputs, CAP usage events (rolled up weekly/monthly) Grading apps, CAP dashboard, update_cap_usage_weekly/monthly On use
YC’s stuff → Monday (WIP) Planned: web forms for coaching log and coaching roster entry, plus a bulk upload feeding the roster — details deliberately TBD for now TBD On submit / on upload

S3 buckets

S3 is the hub, so it is worth knowing which bucket holds what. Fiscal-year buckets are the trap: a job pinned to tl-surveys-27 sees nothing from SY25-26, and a “fall back to last year’s bucket” pattern silently drops the prior year once the current-year object appears. Bind both years rather than falling back.

Bucket Holds Written by Read by
tl-surveys-27 (also -26, -25, and the unsuffixed tl-surveys) Cleaned survey parquet, one object per survey per fiscal year ec2_tl_scripts/update_<survey>.R, update_student_work Nearly every dashboard, report, and email
tl-monday Monday board saves: coaching log, rosters, facilitator board, staffing, logistics lookups, deck list, today’s facilitators update_coaching_log_27, update_coaching_roster_27, save_various_monday_boards, update_fac_board, update_fac_course_list_daily_fy27, update_deck_list Dashboards, trackers, the Qualtrics choice-list jobs, tl-api
coaching-data-tl Coaching-specific derived data (CAPs, coaching outcomes, calendar parquet) Coaching jobs, monday-webhook Coaching dashboards, /calendar endpoints
tl-student-work-27 The actual student work PDFs, one object per uploaded sample update_student_work student_work_samples and student_work_graded apps
survey-ids user_database.parquet — the participant lookup Qualtrics reads at survey open ec2_tl_scripts tl-api /check_email_or_name
tl-shiny-cap-usage-2 CAP usage events plus weekly and monthly rollups CAP dashboard, update_cap_usage_weekly/monthly CAP usage dashboard
tl-file-upload, tl-miscellaneous, tl-daily-stats, tl-capacity-plots, course-assessments-raw Supporting odds and ends: uploaded files, backups, cached stats and plots, raw assessment exports tl_file_upload, Daily Data Backup, assorted jobs The apps that produced them

Operational monitoring

The published inventory below is a design-time view: what exists and when it is supposed to run. The runtime view lives in the Connect Infrastructure Monitor (posit_automations/monitor_connect_scripts.qmd, published at https://tl-data.teachinglab.org/monitor_connect_scripts/, itself a daily scheduled job). It queries the Connect API for every static-rendered script and shows last run time, success rate, run duration trends, and anything overdue by more than 15 minutes.

The two views deliberately cover different sets:

This document Connect Monitor
Scheduled Quarto/R Markdown jobs ✅ the 30 active ones, with cadence and repo path ✅ all of them, with health and timing
Deployed-but-unscheduled static content ❌ omitted ✅ shown (with no next run)
Plumber APIs, Shiny/Streamlit apps ✅ listed with repo paths ❌ out of scope (static content only)
Data flows, buckets, write-backs, event triggers
Per-analyst PDS goal pages ❌ filtered out by name (_pds_)

Most static-rendered content on the server is not scheduled — roughly 200 items against the ~30 in the generated table below (that table carries the exact current count). The unscheduled remainder is one-off analysis renders, FY26 jobs superseded by their FY27 replacements but still deployed (update_coaching_log_26, update_coaching_roster_26, update_coachee_selection, tracker_26, validate_roster_26, update_caps_26), and ~130 per-analyst FY25 PDS goal pages. They cost nothing to leave deployed, but they make the Connect content list hard to read — a periodic sweep is worth scheduling.

CI/CD (GitHub Actions in this repo)

Workflow Trigger What it does
render-posit-automations-index.yml push to posit_automations/ Regenerates posit_automations/INDEX.md
update-architecture.yml weekly + manual Regenerates the inventory below from the Connect API, then syncs this file to the Data Hub as architecture.qmd, renders it, and pushes so Azure redeploys
pkgdown.yaml push Builds package docs (datadocs.teachinglab.org)
run_daily_scripts.yml The old script runner from before everything moved to Posit Connect: it ran the daily SY23-24 automations on a GitHub-hosted Windows runner. Fully commented out and kept for reference

Working with the Connect schedules API

Schedules are owned by /__api__/schedules/{id}. /__api__/variants/{id}/schedules is a read-only collection view and 404s on DELETE, so that is the wrong path to reach for when retiring a job.

A schedule record’s activate: FALSE means “don’t activate the rendered output” — it does not mean the schedule is off. A job with that flag still runs on time. The only reliable way to stop one is to delete the schedule; the content itself can stay deployed and simply becomes unscheduled.

Live Connect inventory

Auto-generated from the Posit Connect API on 2026-08-11 12:36 EDT by data_scripts/generate_architecture.R. Do not edit this section by hand.

Connect server: https://tl-data.teachinglab.org — 265 content items, 30 active schedules, 4 APIs, 29 interactive apps.

Active scheduled jobs (30)

Job Cadence Run Time (ET) Source in Repo
Coach/Facilitator Board Save Daily 01:00 posit_automations/data_updates
Employee Information Daily 00:00 posit_automations/data_updates
Monitor Connect Scripts Daily 12:24 posit_automations
Qualtrics Checks Daily 01:00 posit_automations/validation
Score Course Assessments (FY27) Daily 09:51 posit_automations/data_updates
Staffing Dashboard Targets Workflow Daily 01:00 posit_automations/cap_targets
Update Project Log Daily 00:30 posit_automations/data_updates
Update Qualtrics Survey Teacher Selection (FY27) Daily 03:54 posit_automations/data_updates
daily_percent_completion Daily 00:30 posit_automations/data_updates
save_various_monday_boards Daily 05:58 posit_automations/data_updates
tl_file_upload Daily 19:00 posit_automations/emails
update_coach_selection Daily 04:54 posit_automations/data_updates
update_coaching_roster_27 Daily 00:30 posit_automations/data_updates
update_course_choices Daily 07:00 posit_automations/data_updates
update_deck_list Daily 02:00 posit_automations/data_updates
update_fac_course_list_daily_fy27 Daily 06:00 posit_automations/data_updates
update_facilitator_choices Daily 07:00 posit_automations/data_updates
update_pf_flow Daily 07:00 posit_automations/data_updates
update_site_choices Daily 06:30 posit_automations/data_updates
update_student_outcomes Daily 02:46 posit_automations/data_updates
update_student_work Daily 04:21 posit_automations/data_updates
update_cap_usage_monthly Monthly (day 3) 06:00 posit_automations/data_updates
automated_biweekly_reports Twice monthly 02:38 (not in this repo)
Daily Data Backup Weekdays 14:00 posit_automations/data_updates
FacT Tier Progression Monitoring Weekdays 04:00 (not in this repo)
generate_daily_reports Weekdays 03:58 (not in this repo)
sarah_tierney_custom_design_loops Weekly (Mon) 01:10 posit_automations/misc
update_cap_usage_weekly Weekly (Mon) 06:00 posit_automations/data_updates
managers_automated_weekly_project_log_emails Weekly (Thu) 05:00 posit_automations/emails
project_manager_leads_automated_staffing_emails Weekly (Thu) 05:05 posit_automations/emails

Deployed APIs on Connect

API Last Deployed Source in Repo
monday-webhook 2026-08-03 api/monday-webhook
automated_decks 2026-06-10 api/automated_decks
tracker-webhook 2026-02-10 api/tracker-webhook
tl-survey-comm-api 2025-10-15 api/tl-survey-comm-api

Interactive apps on Connect

Apps not deployed recently may be inactive — check before assuming they are live.

App Framework Last Deployed Source in Repo
student_work_samples shiny 2026-08-11 dashboards/posit-connect/student_work_grading
student_work_graded shiny 2026-08-11 dashboards/posit-connect/student_work_graded
Teaching Lab API Monitor shiny 2026-08-07 dashboards/posit-connect/api_monitor
participant_feedback shiny 2026-08-07 dashboards/posit-connect/participant_feedback
ongoing_report shiny 2026-08-06 dashboards/posit-connect/ongoing_report
tl_individual_feedback shiny 2026-08-05 dashboards/posit-connect/tl_individual_feedback
course_assessments shiny 2026-08-05 dashboards/deprecated/course_assessments
ongoing_report_sidebot shiny 2026-08-04 dashboards/posit-connect/ongoing_report_sidebot
calendar_refresh_monitor shiny 2026-08-03 dashboards/posit-connect/calendar_refresh_monitor
tl_pl_calendar shiny 2026-08-03 dashboards/posit-connect/tl_pl_calendar
cap_usage_dashboard python-streamlit 2026-07-24 (not in this repo)
d9_d11_coaching_outcomes shiny 2026-07-24 dashboards/posit-connect/d9_d11_coaching_outcomes
project_log_tl shiny 2026-07-22 dashboards/posit-connect/project_log_tl
coaching_action_plans_v2 shiny 2026-07-14 dashboards/posit-connect/coaching_action_plans_v2
project_log_report_2 quarto-shiny 2026-05-19 posit_automations/reports
guskey_app shiny 2026-05-11 (not in this repo)
IPGRawDataDashboard shiny 2026-04-13 dashboards/posit-connect/ipg_raw_reports
plumber-logging shiny 2026-03-13 dashboards/posit-connect/plumber-logging
cap_validation shiny 2026-03-11 dashboards/posit-connect/cap_validation
file-upload-tl shiny 2026-02-17 dashboards/posit-connect/file-upload-tl
tl_coteach python-streamlit 2026-02-02 (not in this repo)
ongoing_report_24_25 shiny 2026-01-05 dashboards/posit-connect/ongoing_report_24_25
generate_slide_deck shiny 2025-11-30 dashboards/posit-connect/generate_slide_deck
cap_survey shiny 2025-11-24 (not in this repo)
coaching_action_plans shiny 2025-11-03 analysis/sy25_26/coaching_action_plans
cap_naming_validator shiny 2025-10-16 dashboards/posit-connect/cap_naming_validator
tl_scrollytell shiny 2025-08-07 (not in this repo)
TeachingLLM shiny 2024-06-04 (not in this repo)
nps_monitoring_tl shiny 2024-06-03 (not in this repo)

Disabled schedules still on the server

None.