N8N

n8n streamlit

Two of my favorite technologies I often use are N8n and Streamlit. I thought it would be fun to combine them together in a real-world project.

The idea came to me because many of my N8n workflows begin with form triggers. For example, in past tutorials I’ve built workflows where users fill out a form with keywords, and then N8n scrapes Instagram posts containing those words. While N8n can handle forms, I realized that Streamlit could give us much more flexibility for building interactive data apps and advanced forms.

In this article, we’ll build a Streamlit app that collects form inputs, performs some calculations, sends the data to N8n via a webhook, and then lets N8n run workflows (like scraping Instagram with Apify and storing results in Google Sheets).

Before we start, if you are looking for help with a n8n project, we are taking on customers. Head over to our n8n Automation Engineer page.

Why Streamlit + N8n?

  • Streamlit provides a clean way to build Python-based forms and data apps quickly.

  • N8n excels at orchestrating workflows and integrating third-party services.

  • Together, they give you more control over your frontend inputs (via Streamlit) and backend automation (via N8n).

The Demo Setup

In our demo, we’ll:

  1. Create a form in Streamlit to accept Instagram profile info, campaign budget, and number of posts.

  2. Perform a calculation in Python (cost per post).

  3. Send this data to N8n through a webhook.

  4. Use N8n to run Apify scrapers for Instagram posts and profiles.

  5. Store results in Google Sheets.

How It Works

Step 1: The Streamlit Form

We build a Streamlit app that collects:

  • Instagram profile (username or URL)

  • Number of posts promised

  • Total campaign budget

  • Optional date range

Streamlit also performs a simple calculation:

				
					Cost per post = Campaign budget ÷ Number of posts

				
			

It then displays results like:

  • Asking price

  • Promised posts

  • Cost per post

				
					# https://apify.com/apify/instagram-post-scraper/input-schema
# python -m streamlit run test.py

import os
import json
from datetime import datetime, timezone, timedelta
import streamlit as st
import requests

#os/json – read environment variables & build JSON.
#datetime/timezone/timedelta – handle current time, time zones, and “1 month earlier” default.
#streamlit – build the web UI.
#requests – send HTTP POST to n8n.


st.set_page_config(
    page_title="IG → n8n (Cost per Post)",
    page_icon="📮",
    layout="centered"
)

#Configures the Streamlit page: a title for the browser tab, an icon, and a centered layout.

st.title("Streamlit n8n Demo")
st.caption(
    "Minimal demo: enter an IG profile + promised posts + budget → "
    "compute cost/post → send to n8n webhook for scraping."
)

#Visible heading and short description shown at the top of the app.

def _get_default_webhook_url() -> str:
    if "WEBHOOK_URL" in st.secrets:
        return st.secrets["WEBHOOK_URL"]
    return os.getenv("WEBHOOK_URL", "")

#Looks for a WEBHOOK_URL in Streamlit secrets first (best for production).
#If missing, falls back to the operating system environment variable.

def _coerce_url(url: str) -> str:
    return (url or "").strip()

webhook_url = _get_default_webhook_url()

# Ensure session keys exist
#payload – the JSON that will be sent to n8n.
#calc – numbers used to display metrics (posts, budget, cost per post).

if "payload" not in st.session_state:
    st.session_state.payload = None
if "calc" not in st.session_state:
    st.session_state.calc = None

# --------------------------------------------------------------------
# Main form
# --------------------------------------------------------------------
with st.form("ig_form", clear_on_submit=False):
    ig_profile = st.text_input(
        "Instagram profile (username or full URL)",
        placeholder="creator_handle or https://www.instagram.com/creator_handle/",
        help="No scraping here—just collecting what you enter and sending it to n8n.",
    )


    #Splits the form into two columns
    col1, col2 = st.columns(2)
    with col1:
        promised_posts = st.number_input(
            "Promised posts (count)",
            min_value=1,
            step=1,
            value=3,
            help="How many posts/reels are promised in the collaboration.",
        )
    with col2:
        budget = st.number_input(
            "Total campaign budget ($)",
            min_value=0.01,
            step=50.0,
            value=1200.0,
            help="Used only to compute cost per post on the client side.",
        )

    #Set default time to 1 month earlier
    st.markdown("### Time frame")
    default_cutoff = datetime.now().date() - timedelta(days=30)  # ~1 month earlier
    newer_than = st.date_input(
        "Extract posts newer than",
        value=default_cutoff,
        help="Matches Apify's parameter. Posts strictly newer than this date will be considered.",
    )
    
    submitted = st.form_submit_button("Compute & Prepare Payload")

# --------------------------------------------------------------------
# Handle form submit: validate + compute + persist
# --------------------------------------------------------------------
if submitted:
    errors = []
    if not ig_profile.strip():
        errors.append("Provide an Instagram profile.")
    if promised_posts <= 0:
        errors.append("Promised posts must be at least 1.")
    if budget <= 0:
        errors.append("Budget must be greater than 0.")

    if errors:
        st.error("\n".join(errors))
        st.session_state.payload = None
        st.session_state.calc = None
    elif newer_than and newer_than > datetime.now().date():
        st.error("'Extract posts newer than' cannot be in the future.")
        st.session_state.payload = None
        st.session_state.calc = None
    else:
        # Use the input exactly as provided (username or full URL)
        ig_value = ig_profile.strip()

        cost_per_post = round(float(budget) / float(promised_posts), 2)

        payload = {
            "source": "streamlit-ig-basic",
            "submitted_at": datetime.now(timezone.utc).isoformat(),
            "inputs": {
                "ig_profile": ig_value,
                "promised_posts": int(promised_posts),
                "budget": float(budget),
                "cost_per_post": cost_per_post,
            },
            "scrape_filters": {
                "newer_than": newer_than.isoformat() if newer_than else None,
            },
        }
        st.session_state.payload = payload
        st.session_state.calc = {
            "promised_posts": promised_posts,
            "budget": budget,
            "cost_per_post": cost_per_post,
        }

# --------------------------------------------------------------------
# Always render the “Calculated / Send” section if we have a payload
# --------------------------------------------------------------------
if st.session_state.payload:
    st.subheader("Calculated")
    c1, c2, c3 = st.columns(3)
    c1.metric("Promised posts", st.session_state.calc["promised_posts"])
    c2.metric("Budget ($)", f"${st.session_state.calc['budget']:,.2f}")
    c3.metric("Cost per post ($)", f"${st.session_state.calc['cost_per_post']:,.2f}")

    st.subheader("Payload")
    st.code(json.dumps(st.session_state.payload, indent=2), language="json")

    colA, colB = st.columns([1, 1])
    send_clicked = colA.button("🚀 Send to n8n", type="primary")
    copy_clicked = colB.button("📋 Copy JSON")

    if copy_clicked:
        st.toast(
            "Copy the payload manually from the JSON block above "
            "(automatic copy not supported here)."
        )

    if send_clicked:
        if not webhook_url:
            st.error(
                "Webhook URL not found. Add `WEBHOOK_URL` to Streamlit secrets or set the `WEBHOOK_URL` environment variable."
            )
        else:
            try:
                st.info("Sending payload to n8n…")
                resp = requests.post(
                    _coerce_url(webhook_url),
                    json=st.session_state.payload,
                    timeout=15
                )
                st.write("**Response status:**", resp.status_code)
                try:
                    st.json(resp.json())
                except Exception:
                    st.code(resp.text[:2000])

                if 200 <= resp.status_code < 300:
                    st.success("Sent to n8n successfully ✨")
                else:
                    st.warning("Request sent but got a non-2xx response.")
            except requests.RequestException as e:
                st.error(f"Request failed: {e.__class__.__name__}: {e}")

# --------------------------------------------------------------------
# Footer
# --------------------------------------------------------------------
st.divider()
st.caption(
    "This app computes cost per post locally and forwards IG profile, "
    "promised posts, and date filters to n8n. "
    "Your n8n workflow should handle the Instagram scraping."
)

				
			

Step 2: Sending Data to N8n

When the form is submitted, Streamlit generates a JSON payload and sends it to N8n through an HTTP POST request.

				
					{
  "source": "streamlit-ig-basic",
  "submitted_at": "2025-09-13T15:50:00.123456+00:00",
  "inputs": {
    "ig_profile": "creator_handle",
    "promised_posts": 3,
    "budget": 1200.0,
    "cost_per_post": 400.0
  },
  "scrape_filters": {
    "newer_than": "2025-08-13"
  }
}

				
			

This ensures N8n has all the required data to process the workflow

Step 3: Setting Up the N8n Workflow

Inside N8n, we begin with a Webhook node that captures the incoming form data.

From there, we pass the data through several nodes:

  1. Apify Actor (Profile Scraper) – retrieves profile information.

  2. Apify Actor (Post Scraper) – fetches posts matching specific criteria.

  3. Edit Fields Node – lets you clean, rename, or transform the scraped data fields (e.g., removing unnecessary keys, renaming columns to something more user-friendly, or restructuring objects).

  4. Google Sheets Node – stores the results neatly in a spreadsheet for easy access and analysis.

Step 4: Scraping Instagram with Apify

Using the Apify integration in N8n, we configure two separate actors:

  1. Instagram Profile Scraper – extracts profile details like username, followers, and bio.

  2. Instagram Post Scraper – retrieves recent posts, captions, and engagement metrics.

These scrapers are triggered automatically by N8n whenever the Streamlit form is submitted.

Step 5: Storing Results in Google Sheets

Finally, we connect the workflow to Google Sheets.

Each time a new form submission is made, N8n writes:

  • Profile name

  • Campaign budget

  • Number of posts

  • Cost per post

  • Scraped profile data

  • Scraped posts data

This gives you a structured and easy-to-share dataset.

Security Considerations

When connecting Streamlit to N8n:

  • Store sensitive values (like the webhook URL) in a secrets.toml file for Streamlit.

  • Enable authentication on your N8n webhook to prevent unauthorized submissions.

  • Use HTTPS when deploying both apps to production.

Deployment Options

  • Streamlit can be deployed on Streamlit Community Cloud, Heroku, or your own server.

  • N8n can be self-hosted with Docker or used on N8n Cloud.

  • You can link them securely via environment variables and SSL.

Final Thoughts

By combining Streamlit for frontend input and N8n for backend automation, you can create highly customizable workflows.

This example focused on Instagram campaigns, but the same pattern applies to countless scenarios:

  • Collecting leads from a form and enriching them with APIs

  • Running automated research pipelines

  • Tracking influencer collaborations

  • Building small-scale marketing dashboards

The integration gives you the best of both worlds: an interactive frontend for data collection and a scalable backend for automation.

 

Thank you for reading this article. Make sure to check out our other n8n content on the website. If you need any help with n8n workflows we are taking on customers so reach out!

Leave a Reply

Your email address will not be published. Required fields are marked *