> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cortex.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Link SOC 2 documents to entity

export const RecipeBadges = ({badges = []}) => {
  const map = {
    "recipe": {
      label: "Recipe",
      color: "#A86400"
    },
    "ai-excellence": {
      label: "AI excellence",
      color: "#653EE8"
    },
    "production-readiness": {
      label: "Production readiness",
      color: "#226686"
    },
    "incident-mgmt": {
      label: "Incident management",
      color: "#86198F"
    },
    "migrations": {
      label: "Migrations",
      color: "#0E7C66"
    },
    "compliance": {
      label: "Compliance",
      color: "#475569"
    },
    "dora": {
      label: "DORA metrics",
      color: "#1D4ED8"
    },
    "security": {
      label: "Security",
      color: "#B42318"
    },
    "operational-readiness": {
      label: "Operational readiness",
      color: "#B54708"
    },
    "dev-onboarding": {
      label: "Developer onboarding",
      color: "#C11574"
    }
  };
  const items = badges.map(key => map[key]).filter(Boolean);
  if (items.length === 0) return null;
  return <div className="not-prose flex flex-wrap gap-2 mb-6">
      {items.map(b => <span key={b.label} className="rounded-lg px-2 py-1 text-xs font-medium" style={{
    backgroundColor: b.color,
    color: b.textColor || "#FFFFFF"
  }}>
          {b.label}
        </span>)}
    </div>;
};

<RecipeBadges badges={["recipe", "compliance"]} />

This guide explains how to use a Workflow to streamline the process of linking SOC 2 documents to an entity.

## Create a Workflow to link SOC 2 documents to an entity

### Prerequisites

Before getting started:

* You must have the `Edit Workflows` [permission](/configure/settings/managing-users/permissioning) to create the Workflow, and the `Execute Workflow runs` permission to run it.

### Step 1: Create the Workflow

You can create a Workflow in the Cortex UI or via the Cortex CLI.

<Tabs>
  <Tab title="Cortex CLI">
    #### Add the Workflow via CLI

    You can use the [Cortex CLI](https://pypi.org/project/cortexapps-cli/) to add the example Workflow to your workspace. This allows you to quickly set up the example configuration then iterate on it for your own use case. Expand the tile below to learn more:

    <Accordion title="Import the Workflow via CLI">
      1. Save the Workflow example YAML file below:

      ```yaml theme={null}
      name: Link SOC2 Documents to Entity
      tag: link-soc2-documents-to-an-entity
      description: null
      isDraft: true
      filter:
        entityFilter:
          typeFilter:
            types:
            - service
          entityGroupFilter: null
        ownershipScope: ALL
        type: ENTITY
      runResponseTemplate: null
      failedRunResponseTemplate: null
      restrictActionCompletionToRunnerUser: false
      actions:
      - name: User input
        slug: user-input
        schema:
          inputs:
          - name: Link Type
            description: null
            key: link-type
            required: false
            options:
            - pen-test
            - data-retention-policy
            optionsLabels:
            - Pen Test
            - Data Retention Policy
            defaultValue: null
            placeholder: null
            allowAdditionalOptions: false
            type: SELECT_FIELD
          - name: Link URL
            description: null
            key: link-url
            required: false
            defaultValue: null
            placeholder: null
            validationRegex: null
            type: INPUT_FIELD
          inputOverrides: []
          jsValidatorScript: ""
          type: USER_INPUT
        outgoingActions:
        - retrieve-entity-descriptor
        isRootAction: true
      - name: Retrieve entity descriptor
        slug: retrieve-entity-descriptor
        schema:
          inputs:
            entityId: "{{context.entity.tag}}"
          integrationAlias: null
          actionIdentifier: cortex.retrieveEntityDescriptor
          type: ADVANCED_HTTP_REQUEST
        outgoingActions:
        - javascript
        isRootAction: false
      - name: JavaScript
        slug: javascript
        schema:
          script: |+
            const descriptor = actions['retrieve-entity-descriptor']?.outputs?.descriptor;
            const linkType   = actions['user-input']?.outputs?.['link-type'];
            const linkUrl    = actions['user-input']?.outputs?.['link-url'];

            if (!descriptor || typeof descriptor !== 'string') {
              throw new Error('Descriptor must be a YAML string.');
            }
            const rawType = String(linkType || '').trim();
            const rawUrl  = String(linkUrl || '').trim();
            if (!rawType || !rawUrl) {
              throw new Error('Both link-type and link-url are required.');
            }

            // Normalize input for mapping
            const normalize = s => s.toLowerCase().replace(/[_\s-]+/g, ' ').trim();
            const normType = normalize(rawType);

            let finalType, finalName;
            if (normType === 'pen test' || normType === 'pentest') {
              finalType = 'pen-test';
              finalName = 'Pen Test';
            } else if (normType === 'data retention policy') {
              finalType = 'documentation';
              finalName = rawType; // keep exactly what the user selected
            } else {
              finalType = rawType;
              finalName = rawType;
            }

            // Parse YAML
            let obj;
            try {
              if (typeof YAML.load === 'function') {
                obj = YAML.load(descriptor) || {};
              } else if (typeof YAML.parse === 'function') {
                obj = YAML.parse(descriptor) || {};
              } else {
                throw new Error('YAML parser not available in this runtime.');
              }
            } catch (err) {
              throw new Error('Failed to parse descriptor YAML: ' + err.message);
            }

            // Ensure x-cortex-link exists
            if (!obj.info) obj.info = {};
            const linkKey = 'x-cortex-link';
            const existing = obj.info[linkKey];
            obj.info[linkKey] = Array.isArray(existing) ? existing : (existing ? [existing] : []);

            // Normalize existing links
            const current = obj.info[linkKey]
              .filter(Boolean)
              .map(l => ({
                url: l.url ? String(l.url).trim() : '',
                name: l.name ? String(l.name).trim() : '',
                type: l.type ? String(l.type).trim() : '',
              }))
              .filter(l => l.url && l.name && l.type);

            // Add new link if not duplicate
            const incoming = { url: rawUrl, name: finalName, type: finalType };
            const sig = l => `${l.url}::${l.type}::${l.name}`;
            const seen = new Set(current.map(sig));
            if (!seen.has(sig(incoming))) current.push(incoming);

            obj.info[linkKey] = current.map(l => ({ url: l.url, name: l.name, type: l.type }));

            // Dump back to YAML
            let updatedDescriptor;
            try {
              if (typeof YAML.dump === 'function') {
                updatedDescriptor = YAML.dump(obj, { indent: 2, lineWidth: 120 });
              } else if (typeof YAML.stringify === 'function') {
                updatedDescriptor = YAML.stringify(obj, { indent: 2 });
              } else {
                throw new Error('YAML serializer not available.');
              }
            } catch (err) {
              throw new Error('Failed to serialize descriptor YAML: ' + err.message);
            }

            // ✅ Return updatedDescriptor (not wrapped in {body=...})
            return { updatedDescriptor };

          type: JAVASCRIPT
        outgoingActions:
        - create-or-update-entity
        isRootAction: false
      - name: Create or update entity
        slug: create-or-update-entity
        schema:
          inputs:
            body: "{{{actions.javascript.outputs.result.updatedDescriptor}}}"
            dryRun: false
          integrationAlias: null
          actionIdentifier: cortex.createOrUpdateEntity
          type: ADVANCED_HTTP_REQUEST
        outgoingActions: []
        isRootAction: false
      runRestrictionPolicies: []
      iconTag: SecurityCamera
      variables: []
      ```

      2. Use the [Cortex CLI](https://pypi.org/project/cortexapps-cli/) to run this command, using the path to your Workflow YAML file: `cortex workflows create -f <path-to-your-workflow.yaml>`
    </Accordion>
  </Tab>

  <Tab title="Cortex UI">
    #### Step 1.1: Create the Workflow and configure basic settings

    1. In Cortex, navigate to **Workflows**. In the upper right corner, click **+Create workflow**. Choose a blank Workflow.
    2. Follow the documentation to [configure the basic settings for your Workflow](/streamline/workflows/create#creating-a-workflow).

    #### Step 1.2: Add blocks to the Workflow

    Add the following blocks to your Workflow:

    <AccordionGroup>
      <Accordion title="User input">
        Add a User Input block to collect the link type, link URL, and which entity to update.

        1. Click **+** in the center of the page. In the block library modal, choose **User input**.
        2. In the block configuration side panel, enter a name and unique slug for this block.
           * In this example, we use the name `User input` and the slug `user-input`.
        3. Click **+Add user input**. Add the following:
           * **Name**: Link type
           * **Key**: link-type
           * **Type**: Select
           * **Data source**: Manual
           * **Options**: Click **+Add option** to add options for `Pen Test` and `Data Retention Policy`.
           * Click **Add input**.
        4. Click **+Add user input**. Add the following:
           * **Name**: Link URL
           * **Key**: link-url
           * **Type**: Text
           * Click **Add input**.
        5. At the bottom of the side panel, click **Save**.
      </Accordion>

      <Accordion title="Retrieve entity descriptor">
        This block retrieves the entity descriptor.

        1. Click **+** in the center of the page. In the block library modal, select the **Cortex > Retrieve entity descriptor** block.
        2. In the side panel, enter a name and unique slug for the block.
           * In this example, we use the name `Retrieve entity descriptor` and the slug `retrieve-entity-descriptor`.
        3. Configure the block:
           * **Entity ID or tag**: `{{context.entity.tag}}`
        4. At the bottom of the side panel, click **Save**.
      </Accordion>

      <Accordion title="JavaScript">
        This step retrieves the entity tag, validates required inputs, normalizes the data, and returns the updated link object in YAML format for use in the next Workflow block.

        1. Click **+** in the center of the page. In the block library modal, select the **Cortex > Retrieve entity descriptor** block.
        2. In the side panel, enter a name and unique slug for the block.
           * In this example, we use the name `JavaScript` and the slug `javascript`.
        3. In the text editor under the **JavaScript** header, enter the following:

        ```javascript theme={null}
        const descriptor = actions['retrieve-entity-descriptor']?.outputs?.descriptor;
        const linkType   = actions['user-input']?.outputs?.['link-type'];
        const linkUrl    = actions['user-input']?.outputs?.['link-url'];

        if (!descriptor || typeof descriptor !== 'string') {
          throw new Error('Descriptor must be a YAML string.');
        }
        const rawType = String(linkType || '').trim();
        const rawUrl  = String(linkUrl || '').trim();
        if (!rawType || !rawUrl) {
          throw new Error('Both link-type and link-url are required.');
        }

        // Normalize input for mapping
        const normalize = s => s.toLowerCase().replace(/[_\s-]+/g, ' ').trim();
        const normType = normalize(rawType);

        let finalType, finalName;
        if (normType === 'pen test' || normType === 'pentest') {
          finalType = 'pen-test';
          finalName = 'Pen Test';
        } else if (normType === 'data retention policy') {
          finalType = 'documentation';
          finalName = rawType; // keep exactly what the user selected
        } else {
          finalType = rawType;
          finalName = rawType;
        }

        // Parse YAML
        let obj;
        try {
          if (typeof YAML.load === 'function') {
            obj = YAML.load(descriptor) || {};
          } else if (typeof YAML.parse === 'function') {
            obj = YAML.parse(descriptor) || {};
          } else {
            throw new Error('YAML parser not available in this runtime.');
          }
        } catch (err) {
          throw new Error('Failed to parse descriptor YAML: ' + err.message);
        }

        // Ensure x-cortex-link exists
        if (!obj.info) obj.info = {};
        const linkKey = 'x-cortex-link';
        const existing = obj.info[linkKey];
        obj.info[linkKey] = Array.isArray(existing) ? existing : (existing ? [existing] : []);

        // Normalize existing links
        const current = obj.info[linkKey]
          .filter(Boolean)
          .map(l => ({
            url: l.url ? String(l.url).trim() : '',
            name: l.name ? String(l.name).trim() : '',
            type: l.type ? String(l.type).trim() : '',
          }))
          .filter(l => l.url && l.name && l.type);

        // Add new link if not duplicate
        const incoming = { url: rawUrl, name: finalName, type: finalType };
        const sig = l => `${l.url}::${l.type}::${l.name}`;
        const seen = new Set(current.map(sig));
        if (!seen.has(sig(incoming))) current.push(incoming);

        obj.info[linkKey] = current.map(l => ({ url: l.url, name: l.name, type: l.type }));

        // Dump back to YAML
        let updatedDescriptor;
        try {
          if (typeof YAML.dump === 'function') {
            updatedDescriptor = YAML.dump(obj, { indent: 2, lineWidth: 120 });
          } else if (typeof YAML.stringify === 'function') {
            updatedDescriptor = YAML.stringify(obj, { indent: 2 });
          } else {
            throw new Error('YAML serializer not available.');
          }
        } catch (err) {
          throw new Error('Failed to serialize descriptor YAML: ' + err.message);
        }

        // ✅ Return updatedDescriptor (not wrapped in {body=...})
        return { updatedDescriptor };
        ```

        4. At the bottom of the side panel, click **Save**.
      </Accordion>

      <Accordion title="Create or update entity">
        This block takes the object returned in the previous block and uses it to update the entity.

        1. Click **+** in the center of the page. In the block library modal, select the **Cortex > Create or update entity** block.
        2. In the side panel, enter a name and unique slug for the block.
           * In this example, we use the name `Create or update entity` and the slug `create-or-update-entity`.
        3. Configure the block:
           * **Descriptor**: `{{{actions.javascript.outputs.result.updatedDescriptor}}}`
        4. At the bottom of the side panel, click **Save**.
      </Accordion>
    </AccordionGroup>

    When you are finished adding blocks, click **Save workflow** in the upper right corner of the page.
  </Tab>
</Tabs>

### Step 2: Run the Workflow

* In the [list of Workflows](https://app.getcortexapp.com/admin/workflows), locate the "Link SOC 2 Documents to Entity" Workflow and click **Run**.

When you run the Workflow, the following events happen:

1. The Workflow pauses to collect a response from the user during the User Input block. The user selects an entity, then enters a link type and link URL.
2. The next step runs, retrieving the entity descriptor.
3. The JavaScript block runs, which does the following:
   1. It retrieves the entity descriptor from the previous block, and it retrieves the link type and URL from the initial block. It validates that the required inputs exist.
   2. It normalizes the link type. It parses the descriptor string into a JavaScript object.
   3. It creates a new link object with url, name, and type, and it only adds the new link if it doesn't already exist.
   4. It converts the updated object back to YAML format, and returns `{ updatedDescriptor }` for use in subsequent Workflow blocks.
4. The "Create or update entity" block runs, updating the entity with the `updatedDescriptor` output of the previous block.


## Related topics

- [Ensure SOC 2 compliance](/guides/compliance/soc2.md)
- [Adding external documentation](/ingesting-data-into-cortex/entities-overview/entities/external-docs.md)
- [Workflows](/guides/product-areas/workflows.md)
