linkedin-outreach-intake / streamlit_app.py
nitrolex's picture
Upload streamlit_app.py
6b31350 verified
import os
import io
import time
import requests
import csv
import streamlit as st
# -----------------------------------------------------------------------------
# LinkedIn Outreach Intake App
#
# This Streamlit application provides a simple interface for uploading individual
# LinkedIn contacts or an entire Connections CSV. It forwards the data to two
# n8n webhooks: one for appending contact data to a Google Sheet (intake) and
# another to trigger the outreach workflow that sends email via your Gmail
# account. All configurable values—such as webhook URLs and diagram image
# location—are provided through environment variables so that no secrets are
# hard‑coded in the repository. The outreach itself is handled by n8n and does
# not rely on any paid AI services.
#
# Required environment variables:
# N8N_INTAKE_WEBHOOK – URL of the webhook that writes to Sheet‑1.
# N8N_OUTREACH_WEBHOOK – URL of the webhook that triggers the outreach run.
# Optional environment variables:
# DIAGRAM_URL – Public URL of the flow diagram for display.
#
# To deploy this app in a Hugging Face Space, provide these variables in the
# repository settings (Settings → Variables and secrets).
# -----------------------------------------------------------------------------
INTAKE_URL = os.environ.get("N8N_INTAKE_WEBHOOK")
OUTREACH_URL = os.environ.get("N8N_OUTREACH_WEBHOOK")
DIAGRAM_URL = os.environ.get("DIAGRAM_URL", "")
st.set_page_config(
page_title="LinkedIn Outreach Intake",
page_icon="📇",
layout="centered",
)
st.title("LinkedIn → n8n Intake → Outreach")
if DIAGRAM_URL:
st.image(DIAGRAM_URL, caption="Flow overview", use_column_width=True)
st.write(
"This app lets you add LinkedIn contacts (either individually or via CSV) "
"and trigger the outreach workflow that sends tailored emails."
)
def post_to_intake(data: dict) -> int:
"""Send JSON payload to the intake webhook. Returns HTTP status."""
if not INTAKE_URL:
return 0
try:
resp = requests.post(INTAKE_URL, json=data, timeout=30)
return resp.status_code
except Exception:
return 0
def run_outreach() -> int:
"""Trigger the outreach workflow via its webhook. Returns HTTP status."""
if not OUTREACH_URL:
return 0
try:
resp = requests.post(OUTREACH_URL, json={}, timeout=30)
return resp.status_code
except Exception:
return 0
with st.expander("Add a single contact"):
c1, c2 = st.columns(2)
name = c1.text_input("Name *")
title = c2.text_input("Title *")
company = c1.text_input("Company *")
email = c2.text_input("Email (optional)")
linkedin = st.text_input("LinkedIn URL *")
cbt = st.selectbox("Careers Board Type", ["", "lever", "greenhouse", "url"])
cbk = st.text_input("Careers Board Key (subdomain/slug)")
cbu = st.text_input("Careers URL (for custom boards)")
if st.button("Submit contact"):
# Validate required fields
if not (name and title and company and linkedin):
st.error("Please fill in all required fields marked with *.")
elif not INTAKE_URL:
st.error("N8N_INTAKE_WEBHOOK environment variable not set.")
else:
payload = {
"name": name,
"title": title,
"company": company,
"email": email,
"linkedin_url": linkedin,
"careers_board_type": cbt,
"careers_board_key": cbk,
"careers_url": cbu,
}
status = post_to_intake(payload)
if status == 200:
st.success("Contact saved successfully.")
else:
st.warning(f"Failed to submit: status {status}.")
st.divider()
st.subheader("Bulk import LinkedIn Connections.csv")
csv_file = st.file_uploader(
"Upload your Connections.csv (exported from LinkedIn)",
type=["csv"],
)
st.caption(
"Your CSV should contain at least columns for name, title, company, "
"email, and LinkedIn profile URL. Missing columns will be filled with blanks."
)
if csv_file and st.button("Send CSV to n8n"):
if not INTAKE_URL:
st.error("N8N_INTAKE_WEBHOOK environment variable not set.")
else:
try:
# Reset file pointer and decode the uploaded bytes into a string
csv_bytes = csv_file.getvalue()
csv_text = csv_bytes.decode("utf-8", errors="ignore")
reader = csv.DictReader(io.StringIO(csv_text))
imported = 0
failed = 0
for row in reader:
# Normalize keys: strip whitespace and convert spaces to underscores
normalized = {k.strip().lower().replace(" ", "_"): v for k, v in row.items() if k}
payload = {
"name": normalized.get("name", ""),
"title": normalized.get("title", ""),
"company": normalized.get("company", ""),
"email": normalized.get("email", ""),
# LinkedIn profile URL column may be labelled 'linkedin_url' or 'linkedin'
"linkedin_url": normalized.get("linkedin_url", normalized.get("linkedin", "")),
"careers_board_type": "",
"careers_board_key": "",
"careers_url": "",
}
status = post_to_intake(payload)
if status == 200:
imported += 1
else:
failed += 1
# Sleep briefly to avoid overwhelming n8n
time.sleep(0.25)
st.success(f"Imported {imported} contacts; {failed} failed.")
except Exception as exc:
st.error(f"Error processing CSV: {exc}")
st.divider()
st.subheader("Trigger Outreach Workflow")
st.write(
"When you trigger outreach, the app calls your n8n webhook to read all "
"contacts from Sheet‑1 and send emails. Ensure your sheet is populated "
"and your Gmail credential is configured correctly in n8n before running."
)
if st.button("Run Outreach now"):
if not OUTREACH_URL:
st.error("N8N_OUTREACH_WEBHOOK environment variable not set.")
else:
status = run_outreach()
if status == 200:
st.success("Outreach workflow triggered. Check your n8n logs.")
else:
st.warning(f"Failed to trigger outreach: status {status}.")