# LinkedIn Person Report Agent Workflow ## Overview This workflow builds and deploys a Blocks Network provider agent that accepts a LinkedIn person profile URL, collects public profile details and recent public activity through Apify, and returns a concise markdown report plus structured JSON evidence. The reference implementation is a Node/TypeScript worker hosted on Railway and registered on Blocks as `linkedin_person_report_agent`. The agent is designed for quick relationship research, sales preparation, partnership discovery, executive briefing, and similar business workflows where a short evidence-backed profile summary is more useful than a raw scrape. ## When To Use This - Use this when you need a reusable agent that turns a LinkedIn `/in/` profile URL into a short business-readable report. - Use this when another agent or workflow needs structured LinkedIn profile and activity evidence as a downstream artifact. - Use this when you want a Railway-hosted Blocks provider pattern that can be adapted to similar Apify-backed research agents. ## Prerequisites - Blocks account and provider registration access, so the agent can be registered and called on Blocks. - `BLOCKS_API_KEY`, stored in local `.env` and Railway service variables. Do not commit or print it. - Railway account, Railway CLI authentication, and permission to create or update a Railway project/service. - Apify account with billing or actor access for the LinkedIn actors. - `APIFY_API_TOKEN`, stored in local `.env` and Railway service variables. Do not commit or print it. - Node.js 24 or newer and npm. - Local CLIs: `blocks`, `railway`, and optionally `gh`/`git` if publishing documentation or source changes. - A LinkedIn person profile URL under `/in/`, for example `https://www.linkedin.com/in/satyanadella/`. - No OpenAI API key is required for the current implementation; the report is generated by deterministic TypeScript logic from normalized Apify data. ## Required Inputs - `linkedinUrl`: full LinkedIn person profile URL under `/in/`. - `maxPosts`: maximum posts to analyze. The handler clamps requests to 1-50. - `postedLimit`: one of `any`, `1h`, `24h`, `week`, `month`, `3months`, `6months`, or `year`. - `includeProfileDetails`: whether to fetch public profile metadata. - `includeReposts`: whether to include reposts in activity. - `includeComments`: defaults false because comments increase Apify cost and output size. - `includeReactions`: defaults false because reactions increase Apify cost and output size. Example request: ```json { "linkedinUrl": "https://www.linkedin.com/in/satyanadella/", "maxPosts": 20, "postedLimit": "6months", "includeProfileDetails": true, "includeReposts": true, "includeComments": false, "includeReactions": false } ``` ## Tools And Services - Blocks Network: registers and runs the provider agent so it can be called by users or other agents. - Railway: hosts the long-running Blocks provider worker online. - Apify: runs the LinkedIn profile and activity actors and returns structured datasets. - `harvestapi/linkedin-profile-scraper`: default profile details actor. - `harvestapi/linkedin-profile-posts`: default recent activity actor. - Node/TypeScript: implementation stack for the handler, Apify normalization modules, and trigger script. - GitHub: optional publishing location for reusable workflow documentation or source. ## Project Shape Use this file layout: ```text agent-card.json handler.ts trigger.ts package.json tsconfig.json railway.json .env.example src/linkedinResearch.ts src/profileDetails.ts README.md ``` Important conventions: - `agent-card.json` uses `identity.agentName` with underscores only, for example `linkedin_person_report_agent`. - The package name can use hyphens, for example `linkedin-person-report-agent`. - `railway.json` runs `npm start`, which delegates to `blocks run`. - Keep `.env`, `node_modules`, generated outputs, and logs out of deployment and source control. ## Workflow ### 1. Scaffold Or Copy The Agent Start from the existing project or from a clean template with the files listed above. Keep the Blocks identity boring and explicit: ```json { "identity": { "agentName": "linkedin_person_report_agent", "displayName": "LinkedIn Person Report Agent", "description": "Creates a short report on a LinkedIn person from profile details and recent activity using Apify." } } ``` The current Blocks app URL for the reference agent is: ```text https://app.blocks.ai/agents/linkedin_person_report_agent ``` ### 2. Configure Environment Variables Create local `.env` and Railway variables with these names: ```bash BLOCKS_API_KEY= APIFY_API_TOKEN= APIFY_LINKEDIN_ACTOR_ID=harvestapi/linkedin-profile-posts APIFY_INPUT_TEMPLATE_JSON={"targetUrls":"{{targets}}","maxPosts":"{{maxPosts}}","postedLimit":"{{postedLimit}}","includeQuotePosts":true,"includeReposts":"{{includeReposts}}","scrapeReactions":"{{includeReactions}}","maxReactions":"{{maxReactions}}","postNestedReactions":false,"scrapeComments":"{{includeComments}}","maxComments":"{{maxComments}}","postNestedComments":false} APIFY_DATASET_LIMIT=100 APIFY_LINKEDIN_PROFILE_ACTOR_ID=harvestapi/linkedin-profile-scraper APIFY_PROFILE_SCRAPER_MODE=Profile details no email ($4 per 1k) APIFY_PROFILE_INPUT_TEMPLATE_JSON={"profileScraperMode":"{{profileScraperMode}}","urls":"{{targets}}"} APIFY_PROFILE_DATASET_LIMIT=5 ``` Never print secret values in logs. For Railway, prefer `railway variable set KEY --stdin`. ### 3. Implement The Research Modules Keep scraping code separate from report writing: - `src/profileDetails.ts` calls `harvestapi/linkedin-profile-scraper`, builds actor input from env/template overrides, fetches the default dataset, and normalizes public fields such as name, headline, current company, follower count, connection count, experience, education, and skills. - `src/linkedinResearch.ts` calls `harvestapi/linkedin-profile-posts`, supports actor/template overrides, fetches the default dataset, and normalizes post text, URLs, author fields, dates, engagement counts, and media fields. Normalize multiple possible actor output field names. Do not assume one actor shape will remain stable forever. ### 4. Implement The Blocks Handler In `handler.ts`: 1. Parse a JSON `request` part or a plain LinkedIn `/in/` URL. 2. Validate that the URL is a full LinkedIn person profile URL. 3. Run profile details and activity lookup in parallel. 4. Allow partial success: if activity is empty but profile details exist, return a report with data notes instead of failing. 5. Throw only when no usable profile or activity data is returned. 6. Generate two artifacts: - `person_report` as `text/markdown` - `research_json` as `application/json` The report should include snapshot, activity signals, recurring themes, notable posts, conversation starters, and data notes. ### 5. Validate Locally Run: ```bash npm install npm run typecheck npm run check ``` Confirm credentials are present without printing values: ```bash node -e "require('dotenv').config(); for (const k of ['BLOCKS_API_KEY','APIFY_API_TOKEN']) console.log(k + '=' + (process.env[k] ? 'present' : 'missing'))" ``` Register the provider private/free first: ```bash blocks whoami --json blocks register ``` Start the local worker and send a smoke test: ```bash npm start npm run trigger -- '{"linkedinUrl":"https://www.linkedin.com/in/satyanadella/","maxPosts":5,"postedLimit":"6months","includeProfileDetails":true,"includeReposts":true,"includeComments":false,"includeReactions":false}' ``` If no posts are found, retry with `"postedLimit":"any"` before changing actor configuration. ### 6. Deploy To Railway Check Railway auth and link state: ```bash railway whoami railway status --json ``` Create a project and service when needed: ```bash railway init --name linkedin-person-report-agent --json railway add --service linkedin-person-report-agent --json ``` Set variables without printing values. Use stdin for secrets: ```bash printf '%s' "$BLOCKS_API_KEY" | railway variable set BLOCKS_API_KEY --stdin --skip-deploys --service linkedin-person-report-agent --environment production --json printf '%s' "$APIFY_API_TOKEN" | railway variable set APIFY_API_TOKEN --stdin --skip-deploys --service linkedin-person-report-agent --environment production --json ``` Set the non-secret Apify actor/template configuration as Railway variables too. Deploy: ```bash railway up --detach --json --service linkedin-person-report-agent --environment production --message "Deploy LinkedIn person report Blocks provider" ``` Verify: ```bash railway deployment list --json --service linkedin-person-report-agent --environment production railway logs --latest --lines 200 --service linkedin-person-report-agent --environment production ``` Look for a log line showing the provider registered, such as: ```text registered agent: linkedin_person_report_agent (instance: AG-...) ``` ### 7. Verify The Hosted Blocks Agent Run a hosted task from the project directory: ```bash npm run trigger -- '{"linkedinUrl":"https://www.linkedin.com/in/satyanadella/","maxPosts":5,"postedLimit":"6months","includeProfileDetails":true,"includeReposts":true,"includeComments":false,"includeReactions":false}' ``` Confirm the task completes and returns both artifacts. For a profile without recent posts, rerun with `"postedLimit":"any"`. ## Outputs - Blocks provider agent registered as private/free unless explicitly published otherwise. - Railway worker service running `npm start`. - `person_report` markdown artifact with a short profile/activity brief. - `research_json` artifact with normalized profile details, activity data, actor ids, warnings, request metadata, and generated report markdown. ## Validation The reference implementation was validated with: - `npm run typecheck` - `npm run check` - Local Blocks smoke test - Railway deployment status `SUCCESS` - Hosted task completion returning `person_report` and `research_json` Reference deployment details: - Blocks agent: `linkedin_person_report_agent` - Blocks app URL: `https://app.blocks.ai/agents/linkedin_person_report_agent` - Railway project: `linkedin-person-report-agent` - Railway project ID: `40ff6911-cdba-4716-bb10-6be97094aafc` - Railway service ID: `3a2bd09b-6461-4971-a50b-f46774241984` - Production deployment ID: `dafc043a-2b5d-4e62-ae02-770a792545e7` ## Safety And Quality Notes - Only use public LinkedIn profile/activity data returned by the configured Apify actors. - Do not scrape comments or reactions by default; they add cost and can expose audience-level data that may not be needed. - Do not commit `.env`, API keys, tokens, raw logs with secrets, or private customer data. - Treat Apify outputs as evidence to summarize, not as guaranteed complete truth. Some profiles have no recent public posts, and some fields may be missing. - Keep the JSON artifact for debugging so failures can be diagnosed without repeating paid actor runs. - Use `postedLimit: "any"` as the first fallback when a profile has no recent activity in the default time window.