{
  "name": "Career Radar MarketLens Daily - DRAFT",
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "timezone": "America/New_York",
    "saveExecutionProgress": true,
    "saveManualExecutions": true
  },
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 6
            }
          ]
        }
      },
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.3,
      "position": [
        0,
        0
      ],
      "id": "draft-schedule-trigger",
      "name": "Daily Schedule"
    },
    {
      "parameters": {
        "jsCode": "return [\n  {\n    json: {\n      search_queries: [\n        'GTM engineer',\n        'AI governance analyst'\n      ],\n      lookback_days: 7,\n      prompt_version: 'marketlens-v1',\n      schema_version: '2026-05-05',\n      model_name: 'gpt-4o-mini'\n    }\n  }\n];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        220,
        0
      ],
      "id": "draft-query-config",
      "name": "MarketLens Query Config"
    },
    {
      "parameters": {
        "fieldToSplitOut": "search_queries",
        "options": {}
      },
      "type": "n8n-nodes-base.splitOut",
      "typeVersion": 1,
      "position": [
        440,
        0
      ],
      "id": "draft-split-queries",
      "name": "Split Queries"
    },
    {
      "parameters": {
        "operation": "google_jobs",
        "q": "={{ $json.search_queries }}",
        "additionalFields": {
          "chips": "date_posted:today"
        },
        "requestOptions": {}
      },
      "type": "n8n-nodes-serpapi.serpApi",
      "typeVersion": 1,
      "position": [
        660,
        0
      ],
      "id": "draft-serpapi-search",
      "name": "Google Jobs Search",
      "notes": "Uses the existing SerpApi credential when imported. No credentials are stored in this draft."
    },
    {
      "parameters": {
        "fieldToSplitOut": "jobs_results",
        "options": {}
      },
      "type": "n8n-nodes-base.splitOut",
      "typeVersion": 1,
      "position": [
        880,
        0
      ],
      "id": "draft-split-jobs",
      "name": "Split Jobs"
    },
    {
      "parameters": {
        "jsCode": "const job = $json;\nconst company = (job.company_name || job.company || '').trim();\nconst title = (job.title || '').trim();\nconst description = (job.description || job.detected_extensions?.description || '').trim();\nconst externalJobId = job.job_id || job.share_link || `${title}_${company}`;\n\nif (!company || !title || !description) {\n  return [{\n    json: {\n      ...job,\n      external_job_id: externalJobId,\n      company_name: company,\n      job_title: title,\n      raw_description: description,\n      validation_status: 'missing_required_input',\n      error_message: 'Missing company, title, or description before enrichment.'\n    }\n  }];\n}\n\nreturn [{\n  json: {\n    external_job_id: externalJobId,\n    company_name: company,\n    job_title: title,\n    raw_description: description,\n    job_url: job.share_link || job.link || job.apply_options?.[0]?.link || '',\n    posted_at: job.detected_extensions?.posted_at || job.extensions?.join(', ') || '',\n    source_query: $('Split Queries').item.json.search_queries,\n    prompt_version: $('MarketLens Query Config').first().json.prompt_version,\n    schema_version: $('MarketLens Query Config').first().json.schema_version,\n    model_name: $('MarketLens Query Config').first().json.model_name,\n    validation_status: 'ready'\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1100,
        0
      ],
      "id": "draft-normalize-job",
      "name": "Normalize Job Input"
    },
    {
      "parameters": {
        "jsCode": "const prompt = `Analyze this job posting as a Career Radar labor-market signal.\n\nReturn a single JSON object with:\n- role_title_normalized\n- role_family\n- role_cluster\n- emerging_role_score\n- ai_relevance_score\n- automation_relevance_score\n- company_type\n- industry\n- seniority\n- tools\n- technical_skills\n- business_skills\n- ai_skills\n- responsibilities\n- transformation_category\n- role_evolution_signal\n- less_differentiating_alone_signals\n- market_insight\n- confidence_score\n- evidence_snippets\n\nRules:\n- Use only facts supported by the title, company, description, and source metadata.\n- Keep arrays short and deduplicated.\n- Scores are 1 to 10 except confidence_score, which is 0 to 1.\n- role_family must be exactly one of: Finance, Sales & GTM, Operations, Marketing, Product, HR & People Ops, Risk & Compliance, Data & Analytics, Software & AI, Consulting & Strategy, Other.\n- Pick the closest approved role_family. Do not default to Other if a closer approved bucket is clearly implied.\n- company_type must be exactly one of: Startup, Top Tech, Bank, Consulting, Enterprise SaaS, Healthcare, Government Contractor, Retail, Insurance, Education, Other, Unknown.\n- If the employer appears to be a recruiting/staffing intermediary or job aggregator rather than the true hiring company, classify company_type as Other.\n- If evidence is weak, lower confidence instead of guessing.\n- Return JSON only. No markdown.\n\nRole-family guidance:\n- Finance: ERP, FP&A, accounting systems, finance transformation, budgeting, controllership, NetSuite, SAP finance, Oracle finance.\n- Sales & GTM: GTM systems, RevOps, CRM architecture, Salesforce admin/engineering, revenue systems, lead routing, sales automation, customer lifecycle operations.\n- Operations: workflow automation, business operations, internal systems, process improvement, PMO, service operations, HR or procurement operations when not more specifically HR & People Ops.\n- Marketing: marketing operations, demand gen systems, campaign automation, lifecycle marketing systems, Marketo, HubSpot marketing workflows.\n- Product: product management, product operations, AI product roles, platform product roles, product analytics tied to product ownership.\n- HR & People Ops: HRIS, recruiting operations, people systems, talent operations, workforce tools, Workday HR, Rippling HR, employee lifecycle systems.\n- Risk & Compliance: IAM governance, AI governance, model risk, audit, compliance, privacy, security governance, identity governance.\n- Data & Analytics: analytics engineering, BI, reporting systems, SQL-heavy analyst roles, data activation, reverse ETL, dbt, Snowflake, Databricks.\n- Software & AI: software engineering, internal tools engineering, AI engineering, LLM systems, workflow engines, APIs, automation platforms, agent tooling.\n- Consulting & Strategy: transformation consulting, implementation consulting, strategy and advisory roles where the primary function is client advisory rather than internal ownership.\n\nCompany-type guidance:\n- Startup: earlier-stage software or technology companies, often product-focused and smaller scale.\n- Top Tech: major global technology platforms or frontier AI companies.\n- Bank: banks, asset managers, lenders, major financial institutions.\n- Consulting: consulting, implementation, advisory, systems integrator, staffing, recruiting, or professional-services intermediaries.\n- Enterprise SaaS: established software vendors and B2B software companies.\n- Healthcare: hospitals, payers, providers, pharma, biotech, health services.\n- Government Contractor: federal contractors, defense contractors, public-sector implementation firms.\n- Other: everything else that does not fit clearly.\n- Unknown: only when the employer cannot be reasonably inferred from the posting.\n\nPosting:\nCompany: ${$json.company_name}\nTitle: ${$json.job_title}\nPosted: ${$json.posted_at}\nDescription:\n${$json.raw_description}`;\n\nreturn [{\n  json: {\n    ...$json,\n    openai_system_prompt: 'You analyze job postings as labor-market intelligence signals for workers navigating AI, automation, and software-driven role change. Extract grounded facts separately from inferred market signals. Return only valid JSON matching the requested schema. Do not invent tools, skills, companies, or responsibilities that are not supported by the posting. Use losing differentiation alone framing instead of declaring any skill dead or obsolete. Prefer the closest approved role_family and company_type rather than defaulting to Other when the posting clearly supports a stronger classification.',\n    openai_user_prompt: prompt\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1320,
        0
      ],
      "id": "draft-build-prompt",
      "name": "Build Labor Market Prompt"
    },
    {
      "parameters": {
        "modelId": {
          "__rl": true,
          "value": "gpt-4o-mini",
          "mode": "list",
          "cachedResultName": "GPT-4O-MINI"
        },
        "responses": {
          "values": [
            {
              "role": "system",
              "content": "={{ $json.openai_system_prompt }}"
            },
            {
              "role": "user",
              "content": "={{ $json.openai_user_prompt }}"
            }
          ]
        },
        "builtInTools": {},
        "options": {
          "temperature": 0.2,
          "textFormat": {
            "textOptions": {
              "type": "json_object"
            }
          }
        }
      },
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 2.1,
      "position": [
        1540,
        0
      ],
      "id": "draft-openai-enrich",
      "name": "Message a model",
      "notes": "Draft only. Attach the existing OpenAI credential after importing into n8n."
    },
    {
      "parameters": {
        "jsCode": "const source = $('Build Labor Market Prompt').item.json;\nconst rawModelOutput = $json;\n\nfunction fail(status, message) {\n  return [{\n    json: {\n      ...source,\n      enrichment: null,\n      raw_model_output: rawModelOutput,\n      validation_status: status,\n      error_message: message\n    }\n  }];\n}\n\nfunction maybeParseJson(value) {\n  if (typeof value !== 'string') return value;\n  const trimmed = value.trim();\n  if (!trimmed) return value;\n  if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) return value;\n  return JSON.parse(trimmed);\n}\n\nfunction unwrapOutput(payload) {\n  let candidate = payload;\n\n  if (candidate?.output !== undefined) candidate = candidate.output;\n  candidate = maybeParseJson(candidate);\n\n  if (Array.isArray(candidate)) candidate = candidate[0];\n  if (candidate?.message?.content !== undefined) candidate = candidate.message.content;\n  if (Array.isArray(candidate)) candidate = candidate[0];\n  if (candidate?.content !== undefined) candidate = candidate.content;\n  if (Array.isArray(candidate)) candidate = candidate[0];\n  if (candidate?.text !== undefined) candidate = candidate.text;\n\n  candidate = maybeParseJson(candidate);\n  return candidate;\n}\n\nfunction toLegacyJobFamily(roleFamily) {\n  switch (roleFamily) {\n    case 'Finance':\n      return 'Finance';\n    case 'Sales & GTM':\n      return 'Sales';\n    case 'Operations':\n      return 'Operations';\n    case 'HR & People Ops':\n      return 'Operations';\n    case 'Risk & Compliance':\n      return 'Security';\n    case 'Data & Analytics':\n      return 'Infrastructure';\n    case 'Software & AI':\n      return 'Infrastructure';\n    case 'Marketing':\n      return 'Sales';\n    case 'Product':\n      return 'Other';\n    case 'Consulting & Strategy':\n      return 'Other';\n    default:\n      return 'Other';\n  }\n}\n\nlet parsed;\ntry {\n  parsed = unwrapOutput(rawModelOutput);\n} catch (error) {\n  return fail('invalid_json', error.message);\n}\n\nif (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {\n  return fail('schema_error', 'Model output did not resolve to a single enrichment object');\n}\n\nconst allowedRoleFamilies = ['Finance', 'Sales & GTM', 'Operations', 'Marketing', 'Product', 'HR & People Ops', 'Risk & Compliance', 'Data & Analytics', 'Software & AI', 'Consulting & Strategy', 'Other'];\nconst allowedSeniority = ['Executive', 'Senior', 'Manager', 'IC', 'Early Career', 'Unknown'];\nconst allowedCompanyTypes = ['Startup', 'Top Tech', 'Bank', 'Consulting', 'Enterprise SaaS', 'Healthcare', 'Government Contractor', 'Retail', 'Insurance', 'Education', 'Other', 'Unknown'];\n\nparsed.role_title_normalized = String(parsed.role_title_normalized || source.job_title || '').trim();\nparsed.role_cluster = String(parsed.role_cluster || 'Unclassified').trim();\nparsed.role_family = allowedRoleFamilies.includes(String(parsed.role_family || '').trim()) ? String(parsed.role_family).trim() : 'Other';\nparsed.seniority = allowedSeniority.includes(String(parsed.seniority || '').trim()) ? String(parsed.seniority).trim() : 'Unknown';\nparsed.company_type = allowedCompanyTypes.includes(String(parsed.company_type || '').trim())\n  ? String(parsed.company_type).trim()\n  : (String(parsed.company_type || '').trim() ? 'Other' : 'Unknown');\nparsed.industry = String(parsed.industry || '').trim() || null;\nparsed.transformation_category = String(parsed.transformation_category || '').trim() || null;\nparsed.role_evolution_signal = String(parsed.role_evolution_signal || '').trim() || null;\nparsed.market_insight = String(parsed.market_insight || '').trim() || null;\n\nif (!parsed.role_title_normalized) {\n  return fail('schema_error', 'Missing required enrichment field: role_title_normalized');\n}\n\nconst scoreFields = ['emerging_role_score', 'ai_relevance_score', 'automation_relevance_score'];\nfor (const field of scoreFields) {\n  const value = Number(parsed[field]);\n  if (!Number.isFinite(value) || value < 1 || value > 10) {\n    return fail('schema_error', `Score out of range: ${field}`);\n  }\n  parsed[field] = Math.round(value);\n}\n\nparsed.confidence_score = Number(parsed.confidence_score);\nif (!Number.isFinite(parsed.confidence_score) || parsed.confidence_score < 0 || parsed.confidence_score > 1) {\n  return fail('schema_error', 'confidence_score out of range');\n}\n\nconst arrayFields = ['tools', 'technical_skills', 'business_skills', 'ai_skills', 'responsibilities', 'less_differentiating_alone_signals', 'evidence_snippets'];\nfor (const field of arrayFields) {\n  parsed[field] = Array.isArray(parsed[field]) ? [...new Set(parsed[field].filter(Boolean).map((item) => String(item).trim()))] : [];\n}\n\nreturn [{\n  json: {\n    ...source,\n    enrichment: parsed,\n    legacy_job_family: toLegacyJobFamily(parsed.role_family),\n    raw_model_output: rawModelOutput,\n    validation_status: parsed.confidence_score < 0.55 ? 'low_confidence' : 'valid',\n    error_message: null\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1760,
        0
      ],
      "id": "draft-validate-json",
      "name": "Validate Extraction JSON"
    },
    {
      "parameters": {
        "operation": "upsert",
        "table": "companies",
        "dataSource": "json",
        "json": "={{ JSON.stringify({ name: $('Validate Extraction JSON').item.json.company_name }) }}",
        "sendAllItems": false,
        "onConflict": "name",
        "resolution": "merge-duplicates",
        "returning": "minimal",
        "select": "*"
      },
      "type": "n8n-nodes-supabase-upsert.supabaseUpsert",
      "typeVersion": 1,
      "position": [
        1980,
        -140
      ],
      "id": "draft-upsert-company",
      "name": "Upsert Company Safely",
      "notes": "Community node draft. Upserts the company row by name using the Supabase REST API."
    },
    {
      "parameters": {
        "operation": "upsert",
        "table": "job_signals",
        "dataSource": "json",
        "json": "={{ JSON.stringify({ external_job_id: $('Validate Extraction JSON').item.json.external_job_id, company_name: $('Validate Extraction JSON').item.json.company_name, job_title: $('Validate Extraction JSON').item.json.job_title, raw_description: $('Validate Extraction JSON').item.json.raw_description, job_url: $('Validate Extraction JSON').item.json.job_url, posted_at: $('Validate Extraction JSON').item.json.posted_at, job_family: $('Validate Extraction JSON').item.json.legacy_job_family || 'Other', intent_score: Math.max(1, Math.min(10, Number($('Validate Extraction JSON').item.json.enrichment?.ai_relevance_score || 1))), sales_hook: $('Validate Extraction JSON').item.json.enrichment?.market_insight || '', is_hot_lead: Number($('Validate Extraction JSON').item.json.enrichment?.emerging_role_score || 0) >= 9, score_components: $('Validate Extraction JSON').item.json.enrichment || {} }) }}",
        "sendAllItems": false,
        "onConflict": "external_job_id",
        "resolution": "merge-duplicates",
        "returning": "representation",
        "select": "id,external_job_id,company_name,job_title,created_at"
      },
      "type": "n8n-nodes-supabase-upsert.supabaseUpsert",
      "typeVersion": 1,
      "position": [
        2200,
        0
      ],
      "id": "draft-upsert-job-signal",
      "name": "Upsert Source Posting"
    },
    {
      "parameters": {
        "operation": "upsert",
        "table": "labor_market_enrichments",
        "dataSource": "json",
        "json": "={{ JSON.stringify({ job_signal_id: $json.id, role_title_normalized: $('Validate Extraction JSON').item.json.enrichment?.role_title_normalized || $('Validate Extraction JSON').item.json.job_title, role_family: $('Validate Extraction JSON').item.json.enrichment?.role_family || 'Other', role_cluster: $('Validate Extraction JSON').item.json.enrichment?.role_cluster || 'Unclassified', emerging_role_score: $('Validate Extraction JSON').item.json.enrichment?.emerging_role_score || 1, ai_relevance_score: $('Validate Extraction JSON').item.json.enrichment?.ai_relevance_score || 1, automation_relevance_score: $('Validate Extraction JSON').item.json.enrichment?.automation_relevance_score || 1, company_type: $('Validate Extraction JSON').item.json.enrichment?.company_type || 'Unknown', industry: $('Validate Extraction JSON').item.json.enrichment?.industry || null, seniority: $('Validate Extraction JSON').item.json.enrichment?.seniority || 'Unknown', tools: $('Validate Extraction JSON').item.json.enrichment?.tools || [], technical_skills: $('Validate Extraction JSON').item.json.enrichment?.technical_skills || [], business_skills: $('Validate Extraction JSON').item.json.enrichment?.business_skills || [], ai_skills: $('Validate Extraction JSON').item.json.enrichment?.ai_skills || [], responsibilities: $('Validate Extraction JSON').item.json.enrichment?.responsibilities || [], transformation_category: $('Validate Extraction JSON').item.json.enrichment?.transformation_category || null, role_evolution_signal: $('Validate Extraction JSON').item.json.enrichment?.role_evolution_signal || null, less_differentiating_alone_signals: $('Validate Extraction JSON').item.json.enrichment?.less_differentiating_alone_signals || [], market_insight: $('Validate Extraction JSON').item.json.enrichment?.market_insight || null, confidence_score: $('Validate Extraction JSON').item.json.enrichment?.confidence_score || 0, evidence_snippets: $('Validate Extraction JSON').item.json.enrichment?.evidence_snippets || [], model_name: $('Validate Extraction JSON').item.json.model_name, prompt_version: $('Validate Extraction JSON').item.json.prompt_version, schema_version: $('Validate Extraction JSON').item.json.schema_version, raw_model_output: $('Validate Extraction JSON').item.json.raw_model_output, validation_status: $('Validate Extraction JSON').item.json.validation_status, error_message: $('Validate Extraction JSON').item.json.error_message }) }}",
        "sendAllItems": false,
        "onConflict": "job_signal_id,prompt_version,schema_version",
        "resolution": "merge-duplicates",
        "returning": "representation",
        "select": "id,job_signal_id,validation_status,prompt_version,schema_version"
      },
      "type": "n8n-nodes-supabase-upsert.supabaseUpsert",
      "typeVersion": 1,
      "position": [
        2420,
        0
      ],
      "id": "draft-upsert-enrichment",
      "name": "Upsert Labor Market Enrichment"
    }
  ],
  "connections": {
    "Daily Schedule": {
      "main": [
        [
          {
            "node": "MarketLens Query Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "MarketLens Query Config": {
      "main": [
        [
          {
            "node": "Split Queries",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Queries": {
      "main": [
        [
          {
            "node": "Google Jobs Search",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Jobs Search": {
      "main": [
        [
          {
            "node": "Split Jobs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Jobs": {
      "main": [
        [
          {
            "node": "Normalize Job Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Job Input": {
      "main": [
        [
          {
            "node": "Build Labor Market Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Labor Market Prompt": {
      "main": [
        [
          {
            "node": "Message a model",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Message a model": {
      "main": [
        [
          {
            "node": "Validate Extraction JSON",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Extraction JSON": {
      "main": [
        [
          {
            "node": "Upsert Company Safely",
            "type": "main",
            "index": 0
          },
          {
            "node": "Upsert Source Posting",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upsert Source Posting": {
      "main": [
        [
          {
            "node": "Upsert Labor Market Enrichment",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "meta": {
    "description": "Draft-only workflow export for Career Radar labor-market intelligence. It is intentionally inactive and contains no credentials."
  },
  "tags": [
    {
      "name": "draft"
    },
    {
      "name": "career-radar"
    }
  ]
}