# Link SOC 2 documents to entity

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.md) 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:

<details>

<summary>Import the Workflow via CLI</summary>

1. Save the Workflow example YAML file below:

```yaml
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>`

</details>
{% endtab %}

{% 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.md#step-2-configure-your-workflow-settings).

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

Add the following blocks to your Workflow:

<details>

<summary>User input</summary>

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**.

</details>

<details>

<summary>Retrieve entity descriptor</summary>

This block retrieves the entity descriptor.&#x20;

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**.

</details>

<details>

<summary>JavaScript</summary>

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
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**.

</details>

<details>

<summary>Create or update entity</summary>

This block takes the object returned in the previous block and uses it to update the entity.&#x20;

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**.

</details>

When you are finished adding blocks, click **Save workflow** in the upper right corner of the page.
{% endtab %}
{% endtabs %}

### 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.&#x20;
   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.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.cortex.io/guides/compliance/link-soc2-docs.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
