Skill: jhumanj/twenty-crm (version 1.0.0)

--- name: twenty-crm description: Interact with Twenty CRM (self-hosted) via REST/GraphQL. metadata: {"clawdbot":{"emoji":"🗂️","os":["darwin","linux"]}} --- # Twenty CRM Interact with your self-hosted Twenty instance via REST and GraphQL. ## Config Create `config/twenty.env` (example at `config/twenty.env.example`): - `TWENTY_BASE_URL` (e.g. `https://crm.example.com` or `http://localhost:3000`) - `TWENTY_API_KEY` (Bearer token) Scripts load this file automatically. ## Commands ### Low-level helpers - REST GET: `skills/twenty-crm/scripts/twenty-rest-get.sh "/companies" 'filter={"name":{"ilike":"%acme%"}}&limit=10'` - REST POST: `skills/twenty-crm/scripts/twenty-rest-post.sh "/companies" '{"name":"Acme"}'` - REST PATCH: `skills/twenty-crm/scripts/twenty-rest-patch.sh "/companies/<id>" '{"employees":550}'` - REST DELETE: `skills/twenty-crm/scripts/twenty-rest-delete.sh "/companies/<id>"` - GraphQL: `skills/twenty-crm/scripts/twenty-graphql.sh 'query { companies(limit: 5) { totalCount } }'` ### Common objects (examples) - Create company: `skills/twenty-crm/scripts/twenty-create-company.sh "Acme" "acme.com" 500` - Find companies by name: `skills/twenty-crm/scripts/twenty-find-companies.sh "acme" 10` ## Notes - Twenty supports both REST (`/rest/...`) and GraphQL (`/graphql`). - Object names/endpoints can differ depending on your workspace metadata and Twenty version. - Auth tokens can be short-lived depending on your setup; refresh if you get `401`.

path: scripts/twenty-config.sh
content: #!/bin/bash
set -euo pipefail

# Loads config for Twenty CRM API scripts.
# Expected file: /Users/jhumanj/clawd/config/twenty.env

CONFIG_FILE="/Users/jhumanj/clawd/config/twenty.env"

if [ -f "$CONFIG_FILE" ]; then
  # shellcheck disable=SC1090
  source "$CONFIG_FILE"
fi

if [ -z "${TWENTY_BASE_URL:-}" ]; then
  echo "Missing TWENTY_BASE_URL. Create $CONFIG_FILE (see /Users/jhumanj/clawd/config/twenty.env.example)." >&2
  exit 1
fi

if [ -z "${TWENTY_API_KEY:-}" ]; then
  echo "Missing TWENTY_API_KEY. Create $CONFIG_FILE (see /Users/jhumanj/clawd/config/twenty.env.example)." >&2
  exit 1
fi

# Normalize trailing slash
TWENTY_BASE_URL="${TWENTY_BASE_URL%/}"

export TWENTY_BASE_URL
export TWENTY_API_KEY


path: scripts/twenty-create-company.sh
content: #!/bin/bash
set -euo pipefail

# Convenience wrapper using REST.
# Usage: twenty-create-company.sh "Name" [domain] [employees]

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}" )" && pwd)"

NAME="${1:-}"
DOMAIN="${2:-}"
EMPLOYEES="${3:-}"

if [ -z "$NAME" ]; then
  echo "Usage: twenty-create-company.sh \"Name\" [domain] [employees]" >&2
  exit 1
fi

python3 - <<'PY' "$NAME" "$DOMAIN" "$EMPLOYEES" > /tmp/twenty_create_company.json
import json,sys
name,domain,employees = sys.argv[1],sys.argv[2],sys.argv[3]
payload = {"name": name}
if domain:
  payload["domainName"] = domain
if employees:
  try:
    payload["employees"] = int(employees)
  except ValueError:
    payload["employees"] = employees
print(json.dumps(payload))
PY

"$SCRIPT_DIR/twenty-rest-post.sh" "/companies" "$(cat /tmp/twenty_create_company.json)"


path: scripts/twenty-find-companies.sh
content: #!/bin/bash
set -euo pipefail

# Usage: twenty-find-companies.sh "search" [limit]
# Uses REST filter with ilike (may vary by Twenty version/workspace).

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}" )" && pwd)"

SEARCH="${1:-}"
LIMIT="${2:-10}"

if [ -z "$SEARCH" ]; then
  echo "Usage: twenty-find-companies.sh \"search\" [limit]" >&2
  exit 1
fi

FILTER=$(python3 - <<'PY' "$SEARCH"
import json,sys
s=sys.argv[1]
print(json.dumps({"name":{"ilike":f"%{s}%"}}))
PY
)

# Querystring is not URL-encoded here; keep search terms simple.
"$SCRIPT_DIR/twenty-rest-get.sh" "/companies" "filter=$FILTER&limit=$LIMIT&offset=0"


path: scripts/twenty-graphql.sh
content: #!/bin/bash
set -euo pipefail

# Usage: twenty-graphql.sh 'query { companies(limit: 5) { totalCount } }' [variablesJson]

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}" )" && pwd)"
# shellcheck disable=SC1091
source "$SCRIPT_DIR/twenty-config.sh"

QUERY="${1:-}"
VARIABLES="${2:-}"

if [ -z "$QUERY" ]; then
  echo "Usage: twenty-graphql.sh 'query { ... }' [variablesJson]" >&2
  exit 1
fi

QUERY_JSON=$(python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$QUERY")

if [ -n "$VARIABLES" ]; then
  BODY=$(printf '{"query":%s,"variables":%s}' "$QUERY_JSON" "$VARIABLES")
else
  BODY=$(printf '{"query":%s}' "$QUERY_JSON")
fi

curl -sS -X POST "$TWENTY_BASE_URL/graphql" \
  -H "Authorization: Bearer $TWENTY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  --data "$BODY"


path: scripts/twenty-rest-delete.sh
content: #!/bin/bash
set -euo pipefail

# Usage: twenty-rest-delete.sh "/companies/<id>" [destroy]
# If second arg is "destroy", calls the /destroy endpoint.

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}" )" && pwd)"
# shellcheck disable=SC1091
source "$SCRIPT_DIR/twenty-config.sh"

PATH_PART="${1:-}"
MODE="${2:-}"

if [ -z "$PATH_PART" ]; then
  echo "Usage: twenty-rest-delete.sh \"/path\" [destroy]" >&2
  exit 1
fi

URL="$TWENTY_BASE_URL/rest${PATH_PART}"
if [ "$MODE" = "destroy" ]; then
  URL="$URL/destroy"
fi

curl -sS -X DELETE "$URL" \
  -H "Authorization: Bearer $TWENTY_API_KEY" \
  -H "Accept: application/json"


path: scripts/twenty-rest-get.sh
content: #!/bin/bash
set -euo pipefail

# Usage: twenty-rest-get.sh "/companies" [queryString]
# Example: twenty-rest-get.sh "/companies" 'limit=10&offset=0'

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091
source "$SCRIPT_DIR/twenty-config.sh"

PATH_PART="${1:-}"
QUERY="${2:-}"

if [ -z "$PATH_PART" ]; then
  echo "Usage: twenty-rest-get.sh \"/path\" [queryString]" >&2
  exit 1
fi

URL="$TWENTY_BASE_URL/rest${PATH_PART}"
if [ -n "$QUERY" ]; then
  URL="$URL?$QUERY"
fi

curl -sS "$URL" \
  -H "Authorization: Bearer $TWENTY_API_KEY" \
  -H "Accept: application/json"


path: scripts/twenty-rest-patch.sh
content: #!/bin/bash
set -euo pipefail

# Usage: twenty-rest-patch.sh "/companies/<id>" '{"employees":123}'

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}" )" && pwd)"
# shellcheck disable=SC1091
source "$SCRIPT_DIR/twenty-config.sh"

PATH_PART="${1:-}"
JSON_BODY="${2:-}"

if [ -z "$PATH_PART" ] || [ -z "$JSON_BODY" ]; then
  echo "Usage: twenty-rest-patch.sh \"/path\" '{...json...}'" >&2
  exit 1
fi

curl -sS -X PATCH "$TWENTY_BASE_URL/rest${PATH_PART}" \
  -H "Authorization: Bearer $TWENTY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  --data "$JSON_BODY"


path: scripts/twenty-rest-post.sh
content: #!/bin/bash
set -euo pipefail

# Usage: twenty-rest-post.sh "/companies" '{"name":"Acme"}'

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}" )" && pwd)"
# shellcheck disable=SC1091
source "$SCRIPT_DIR/twenty-config.sh"

PATH_PART="${1:-}"
JSON_BODY="${2:-}"

if [ -z "$PATH_PART" ] || [ -z "$JSON_BODY" ]; then
  echo "Usage: twenty-rest-post.sh \"/path\" '{...json...}'" >&2
  exit 1
fi

curl -sS -X POST "$TWENTY_BASE_URL/rest${PATH_PART}" \
  -H "Authorization: Bearer $TWENTY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  --data "$JSON_BODY"


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: suspicious | SkillSpector: suspicious (MEDIUM)