AutomationFlowsWeb Scraping › Token Check

Token Check

token-check. Uses httpRequest. Scheduled trigger; 13 nodes.

Cron / scheduled trigger★★★★☆ complexity13 nodesHTTP Request
Web Scraping Trigger: Cron / scheduled Nodes: 13 Complexity: ★★★★☆ Added:

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "id": "token-check",
  "name": "token-check",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 9 * * *"
            }
          ]
        }
      },
      "id": "schedule-trigger",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1,
      "position": [
        240,
        200
      ]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "token-check",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "webhook-trigger",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        240,
        460
      ]
    },
    {
      "parameters": {
        "workflowId": "hmac-verify",
        "options": {},
        "workflowInputs": {
          "value": {
            "headers": "={{ $json.headers }}",
            "body": "={{ $json.body }}",
            "rawBody": "={{ $json.body ? JSON.stringify($json.body) : '' }}"
          }
        }
      },
      "id": "hmac-check",
      "name": "HMAC Verify",
      "type": "n8n-nodes-base.executeWorkflow",
      "typeVersion": 1,
      "position": [
        460,
        460
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "05af1013-37eb-4119-9314-d3ddfd1f63cd",
              "leftValue": "={{ $json.verified }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true"
              }
            }
          ],
          "combinator": "and"
        }
      },
      "id": "if-verified",
      "name": "Is Verified?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        680,
        460
      ]
    },
    {
      "parameters": {
        "jsCode": "// T040-T042: Dual-token tracking, automated refresh, refresh token monitoring\n// LinkedIn access tokens: 60-day TTL\n// LinkedIn refresh tokens: 365-day TTL\n// Consolidated logic shared by Schedule and Webhook paths\n\nconst staticData = $getWorkflowStaticData('global');\n\n// 019: Read hook token from Static Data (not $env)\nconst hookToken = staticData.hookToken || '';\nconst now = new Date();\nconst ONE_DAY_MS = 1000 * 60 * 60 * 24;\n\n// --- Initialize timestamps on first run ---\nif (!staticData.access_token_granted_at) {\n  // Migrate from legacy grant_timestamp if present\n  if (staticData.grant_timestamp) {\n    staticData.access_token_granted_at = staticData.grant_timestamp;\n    staticData.refresh_token_granted_at = staticData.grant_timestamp;\n    delete staticData.grant_timestamp;\n  } else {\n    staticData.access_token_granted_at = now.toISOString();\n    staticData.refresh_token_granted_at = now.toISOString();\n  }\n}\nif (!staticData.refresh_token_granted_at) {\n  staticData.refresh_token_granted_at = staticData.access_token_granted_at;\n}\n\n// --- Compute days remaining for both tokens ---\nconst accessGrantDate = new Date(staticData.access_token_granted_at);\nconst refreshGrantDate = new Date(staticData.refresh_token_granted_at);\n\nconst accessDaysSinceGrant = Math.floor((now - accessGrantDate) / ONE_DAY_MS);\nconst refreshDaysSinceGrant = Math.floor((now - refreshGrantDate) / ONE_DAY_MS);\n\nconst access_token_days_remaining = Math.max(60 - accessDaysSinceGrant, 0);\nconst refresh_token_days_remaining = Math.max(365 - refreshDaysSinceGrant, 0);\n\nconst accessExpiryDate = new Date(accessGrantDate);\naccessExpiryDate.setDate(accessExpiryDate.getDate() + 60);\n\nconst refreshExpiryDate = new Date(refreshGrantDate);\nrefreshExpiryDate.setDate(refreshExpiryDate.getDate() + 365);\n\n// --- Initialize retry and flag state ---\nif (typeof staticData.refresh_retry_count !== 'number') {\n  staticData.refresh_retry_count = 0;\n}\nif (typeof staticData.refresh_in_progress !== 'boolean') {\n  staticData.refresh_in_progress = false;\n}\nif (typeof staticData.refresh_token_expired !== 'boolean') {\n  staticData.refresh_token_expired = false;\n}\n\n// --- T042: Refresh token expiry monitoring ---\nlet alert_type = null;\nlet alert_message = null;\nlet alert_severity = 'info';\n\nif (refresh_token_days_remaining <= 0) {\n  staticData.refresh_token_expired = true;\n  alert_type = 'refresh_token_expired';\n  alert_message = 'LinkedIn refresh token has expired. Manual re-authorization required.';\n  alert_severity = 'critical';\n} else if (refresh_token_days_remaining <= 30) {\n  alert_type = 'refresh_token_expiring';\n  alert_message = `LinkedIn refresh token expires in ${refresh_token_days_remaining} days (${refreshExpiryDate.toISOString().split('T')[0]}). Plan re-authorization.`;\n  alert_severity = 'warning';\n}\n\n// --- T041: Determine if automated refresh is needed ---\nlet should_refresh = false;\n\nif (access_token_days_remaining <= 7\n    && !staticData.refresh_in_progress\n    && !staticData.refresh_token_expired\n    && refresh_token_days_remaining > 0) {\n  should_refresh = true;\n}\n\n// --- T041: Access token alert (only if refresh not possible) ---\nif (access_token_days_remaining <= 7 && !should_refresh && !alert_type) {\n  alert_type = 'token_expiring';\n  alert_message = `LinkedIn access token expires in ${access_token_days_remaining} days (${accessExpiryDate.toISOString().split('T')[0]}). Automated refresh unavailable \u2014 manual re-authorization required.`;\n  alert_severity = 'warning';\n}\n\n// Determine overall status\nlet status = 'healthy';\nif (refresh_token_days_remaining <= 0) {\n  status = 'refresh_token_expired';\n} else if (access_token_days_remaining <= 0) {\n  status = 'expired';\n} else if (access_token_days_remaining <= 7) {\n  status = 'expiring_soon';\n} else if (refresh_token_days_remaining <= 30) {\n  status = 'refresh_expiring_soon';\n}\n\n// Set refresh_in_progress flag before passing to refresh node\nif (should_refresh) {\n  staticData.refresh_in_progress = true;\n}\n\nreturn [{\n  json: {\n    status,\n    access_token_days_remaining,\n    access_token_grant_date: accessGrantDate.toISOString().split('T')[0],\n    access_token_expiry_date: accessExpiryDate.toISOString().split('T')[0],\n    refresh_token_days_remaining,\n    refresh_token_grant_date: refreshGrantDate.toISOString().split('T')[0],\n    refresh_token_expiry_date: refreshExpiryDate.toISOString().split('T')[0],\n    should_refresh,\n    refresh_in_progress: staticData.refresh_in_progress,\n    refresh_token_expired: staticData.refresh_token_expired,\n    alert_needed: !!alert_type,\n    alert_type,\n    alert_message,\n    alert_severity\n  }\n}];"
      },
      "id": "check-token",
      "name": "Check Token",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        460,
        200
      ],
      "notes": "T040-T042: Consolidated dual-token check. Computes days remaining for access (60d) and refresh (365d) tokens. Sets should_refresh when access token <= 7 days and refresh token is valid."
    },
    {
      "parameters": {
        "jsCode": "// Check Token (Webhook) \u2014 delegates to same logic as Schedule path\n// This node exists because the webhook path needs its own entry point\n// but the logic is identical to the Schedule path's Check Token node.\n\nconst staticData = $getWorkflowStaticData('global');\nconst now = new Date();\nconst ONE_DAY_MS = 1000 * 60 * 60 * 24;\n\n// --- Initialize timestamps on first run ---\nif (!staticData.access_token_granted_at) {\n  if (staticData.grant_timestamp) {\n    staticData.access_token_granted_at = staticData.grant_timestamp;\n    staticData.refresh_token_granted_at = staticData.grant_timestamp;\n    delete staticData.grant_timestamp;\n  } else {\n    staticData.access_token_granted_at = now.toISOString();\n    staticData.refresh_token_granted_at = now.toISOString();\n  }\n}\nif (!staticData.refresh_token_granted_at) {\n  staticData.refresh_token_granted_at = staticData.access_token_granted_at;\n}\n\nconst accessGrantDate = new Date(staticData.access_token_granted_at);\nconst refreshGrantDate = new Date(staticData.refresh_token_granted_at);\n\nconst accessDaysSinceGrant = Math.floor((now - accessGrantDate) / ONE_DAY_MS);\nconst refreshDaysSinceGrant = Math.floor((now - refreshGrantDate) / ONE_DAY_MS);\n\nconst access_token_days_remaining = Math.max(60 - accessDaysSinceGrant, 0);\nconst refresh_token_days_remaining = Math.max(365 - refreshDaysSinceGrant, 0);\n\nconst accessExpiryDate = new Date(accessGrantDate);\naccessExpiryDate.setDate(accessExpiryDate.getDate() + 60);\n\nconst refreshExpiryDate = new Date(refreshGrantDate);\nrefreshExpiryDate.setDate(refreshExpiryDate.getDate() + 365);\n\nlet status = 'healthy';\nif (refresh_token_days_remaining <= 0) {\n  status = 'refresh_token_expired';\n} else if (access_token_days_remaining <= 0) {\n  status = 'expired';\n} else if (access_token_days_remaining <= 7) {\n  status = 'expiring_soon';\n} else if (refresh_token_days_remaining <= 30) {\n  status = 'refresh_expiring_soon';\n}\n\nreturn [{\n  json: {\n    status,\n    access_token_days_remaining,\n    access_token_grant_date: accessGrantDate.toISOString().split('T')[0],\n    access_token_expiry_date: accessExpiryDate.toISOString().split('T')[0],\n    refresh_token_days_remaining,\n    refresh_token_grant_date: refreshGrantDate.toISOString().split('T')[0],\n    refresh_token_expiry_date: refreshExpiryDate.toISOString().split('T')[0],\n    refresh_token_expired: !!staticData.refresh_token_expired,\n    last_refresh_result: staticData.last_refresh_result || 'not_attempted'\n  }\n}];"
      },
      "id": "check-token-webhook",
      "name": "Check Token (Webhook)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        900,
        400
      ],
      "notes": "Webhook path: returns dual-token status as JSON response. Read-only \u2014 does not trigger refresh (refresh is schedule-only)."
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify($json) }}",
        "options": {
          "responseCode": 200
        }
      },
      "id": "respond-success",
      "name": "Respond Success",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1,
      "position": [
        1120,
        400
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ error: 'Unauthorized', details: $json.error }) }}",
        "options": {
          "responseCode": 401
        }
      },
      "id": "respond-401",
      "name": "Reject 401",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1,
      "position": [
        900,
        580
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "c1a2b3d4-5678-9012-abcd-ef0123456789",
              "leftValue": "={{ $json.should_refresh }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true"
              }
            }
          ],
          "combinator": "and"
        }
      },
      "id": "if-should-refresh",
      "name": "Should Refresh?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        680,
        200
      ],
      "notes": "T041: Routes to OAuth refresh when access token <= 7 days and refresh token is valid."
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://www.linkedin.com/oauth/v2/accessToken",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/x-www-form-urlencoded"
            }
          ]
        },
        "sendBody": true,
        "contentType": "form-urlencoded",
        "bodyParameters": {
          "parameters": [
            {
              "name": "grant_type",
              "value": "refresh_token"
            },
            {
              "name": "refresh_token",
              "value": "={{ $credentials.linkedInOAuth2Api.refreshToken }}"
            },
            {
              "name": "client_id",
              "value": "={{ $credentials.linkedInOAuth2Api.clientId }}"
            },
            {
              "name": "client_secret",
              "value": "={{ $credentials.linkedInOAuth2Api.clientSecret }}"
            }
          ]
        },
        "options": {
          "response": {
            "response": {
              "fullResponse": true
            }
          },
          "timeout": 30000
        }
      },
      "id": "refresh-token-request",
      "name": "Refresh Access Token",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4,
      "position": [
        900,
        140
      ],
      "notes": "T041: POST to LinkedIn OAuth endpoint to refresh the access token. Uses n8n credential expressions for client_id, client_secret, and refresh_token.",
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "// T041: Handle OAuth refresh response\n// Processes success and error cases from the LinkedIn token refresh\n\nconst staticData = $getWorkflowStaticData('global');\nconst input = $input.first().json;\nconst now = new Date();\n\n// Clear refresh_in_progress flag\nstaticData.refresh_in_progress = false;\n\n// Check if the HTTP request succeeded\nconst statusCode = input.statusCode || (input.headers ? 200 : 0);\nconst body = input.body || input;\n\nlet result = {\n  refresh_success: false,\n  alert_needed: false,\n  alert_type: null,\n  alert_message: null,\n  alert_severity: 'error'\n};\n\nif (statusCode >= 200 && statusCode < 300 && body.access_token) {\n  // --- SUCCESS: Update grant timestamp ---\n  staticData.access_token_granted_at = now.toISOString();\n  staticData.last_refresh_attempt = now.toISOString();\n  staticData.last_refresh_result = 'success';\n  staticData.refresh_retry_count = 0;\n\n  result.refresh_success = true;\n  result.alert_needed = false;\n  result.new_access_token_expiry = new Date(now.getTime() + 60 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];\n\n} else {\n  // --- FAILURE: Classify error and determine retry/alert ---\n  staticData.last_refresh_attempt = now.toISOString();\n  staticData.last_refresh_result = 'failed';\n\n  const errorType = body.error || '';\n  const errorDescription = body.error_description || '';\n\n  if (errorType === 'invalid_grant') {\n    // Refresh token revoked or expired \u2014 no retry, alert immediately\n    staticData.refresh_token_expired = true;\n    staticData.refresh_retry_count = 0;\n    result.alert_needed = true;\n    result.alert_type = 'token_refresh_failed';\n    result.alert_message = `LinkedIn token refresh failed: refresh token revoked or expired. Manual re-authorization required. (${errorDescription})`;\n    result.alert_severity = 'critical';\n\n  } else if (errorType === 'invalid_client') {\n    // Client credentials wrong \u2014 no retry, alert immediately\n    staticData.refresh_retry_count = 0;\n    result.alert_needed = true;\n    result.alert_type = 'token_refresh_failed';\n    result.alert_message = `LinkedIn token refresh failed: invalid client credentials. Verify client_id and client_secret in n8n LinkedIn OAuth2 credential. (${errorDescription})`;\n    result.alert_severity = 'critical';\n\n  } else if (statusCode >= 500) {\n    // Server error \u2014 retry up to 3 times\n    staticData.refresh_retry_count = (staticData.refresh_retry_count || 0) + 1;\n    if (staticData.refresh_retry_count >= 3) {\n      result.alert_needed = true;\n      result.alert_type = 'token_refresh_failed';\n      result.alert_message = `LinkedIn token refresh failed after 3 retries (HTTP ${statusCode}). Server-side error \u2014 will retry on next scheduled run.`;\n      result.alert_severity = 'error';\n      staticData.refresh_retry_count = 0;\n    } else {\n      // Will retry on next scheduled run\n      result.alert_needed = false;\n      result.retry_pending = true;\n      result.retry_count = staticData.refresh_retry_count;\n    }\n\n  } else {\n    // Network error or unknown \u2014 retry once then alert\n    staticData.refresh_retry_count = (staticData.refresh_retry_count || 0) + 1;\n    if (staticData.refresh_retry_count >= 2) {\n      result.alert_needed = true;\n      result.alert_type = 'token_refresh_failed';\n      result.alert_message = `LinkedIn token refresh failed after retry (${errorType || 'network/unknown error'}). ${errorDescription || 'Check network connectivity and LinkedIn API status.'}`;\n      result.alert_severity = 'error';\n      staticData.refresh_retry_count = 0;\n    } else {\n      result.alert_needed = false;\n      result.retry_pending = true;\n      result.retry_count = staticData.refresh_retry_count;\n    }\n  }\n}\n\nreturn [{ json: result }];"
      },
      "id": "handle-refresh-response",
      "name": "Handle Refresh Response",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1120,
        140
      ],
      "notes": "T041: Classifies refresh result \u2014 updates Static Data on success, implements circuit breaker and retry logic on failure."
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "aabb1122-3344-5566-7788-99aabbccddee",
              "leftValue": "={{ $json.alert_needed }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true"
              }
            }
          ],
          "combinator": "and"
        }
      },
      "id": "if-alert-needed",
      "name": "Alert Needed?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1340,
        200
      ]
    },
    {
      "parameters": {
        "url": "http://host.docker.internal:18789/hooks/agent",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer {{ $json.hookToken }}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "payload",
              "value": "={{ JSON.stringify({ type: 'alert', alert_type: $json.alert_type || 'token_expiring', message: $json.alert_message || ('LinkedIn OAuth token status: ' + $json.status), severity: $json.alert_severity || 'warning', access_token_days_remaining: $json.access_token_days_remaining, refresh_token_days_remaining: $json.refresh_token_days_remaining, access_token_expiry_date: $json.access_token_expiry_date }) }}"
            }
          ]
        },
        "options": {}
      },
      "id": "send-alert",
      "name": "Alert OpenClaw",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4,
      "position": [
        1560,
        140
      ],
      "notes": "Sends token lifecycle alerts to OpenClaw inbound hook. Alert types: token_expiring, refresh_token_expiring, refresh_token_expired, token_refresh_failed."
    }
  ],
  "connections": {
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Check Token",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Token": {
      "main": [
        [
          {
            "node": "Should Refresh?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Should Refresh?": {
      "main": [
        [
          {
            "node": "Refresh Access Token",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Alert Needed?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Refresh Access Token": {
      "main": [
        [
          {
            "node": "Handle Refresh Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Handle Refresh Response": {
      "main": [
        [
          {
            "node": "Alert Needed?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Alert Needed?": {
      "main": [
        [
          {
            "node": "Alert OpenClaw",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Webhook": {
      "main": [
        [
          {
            "node": "HMAC Verify",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HMAC Verify": {
      "main": [
        [
          {
            "node": "Is Verified?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Verified?": {
      "main": [
        [
          {
            "node": "Check Token (Webhook)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Reject 401",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Token (Webhook)": {
      "main": [
        [
          {
            "node": "Respond Success",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "staticData": null,
  "meta": {
    "notes": "T040-T042: LinkedIn OAuth dual-token lifecycle manager. Tracks both access tokens (60-day TTL) and refresh tokens (365-day TTL). Schedule path (daily 09:00): checks token status, auto-refreshes access token when <= 7 days remaining, alerts on refresh token expiry (30-day warning), circuit breaker on refresh failure. Webhook path: returns read-only token status. Grant timestamps stored in Workflow Static Data."
  }
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

token-check. Uses httpRequest. Scheduled trigger; 13 nodes.

Source: https://github.com/traylorre/openclaw-mac/blob/d5ef2bd70e230b1877d7165d35c2e27947b269e0/workflows/token-check.json — original creator credit. Request a take-down →

More Web Scraping workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Web Scraping

Birthday Automation - Production (Fixed). Uses stopAndError, httpRequest, emailSend, bannerbear. Scheduled trigger; 86 nodes.

Stop And Error, HTTP Request, Email Send +1
Web Scraping

This template runs two scheduled workflows to govern Microsoft Entra ID (Azure AD) guest accounts by detecting stale users via Microsoft Graph, staging deletions in SharePoint with a 72-hour window, n

Microsoft SharePoint, Microsoft Teams, Microsoft Entra +1
Web Scraping

Jira-Allure-Auto-Qa. Uses httpRequest, jira. Scheduled trigger; 68 nodes.

HTTP Request, Jira
Web Scraping

Spotify-Sync-Surrealdb-V1. Uses httpRequest, n8n-nodes-surrealdb, spotify. Scheduled trigger; 62 nodes.

HTTP Request, N8N Nodes Surrealdb, Spotify
Web Scraping

As n8n instances scale, teams often lose track of sub-workflows—who uses them, where they are referenced, and whether they can be safely updated. This leads to inefficiencies like unnecessary copies o

HTTP Request, n8n, N8N Trigger +1