Real-world examples of integrating Exocortex CLI with CI/CD pipelines, external tools, and automated workflows.
- GitHub Actions
- GitLab CI/CD
- Automated Task Creation
- External Tool Integration
- Webhook Automation
- Scheduled Maintenance
Automatically create Exocortex tasks when GitHub issues are opened.
.github/workflows/issue-to-task.yml:
name: Create Task from Issue
on:
issues:
types: [opened]
jobs:
create-task:
runs-on: ubuntu-latest
steps:
- name: Checkout vault
uses: actions/checkout@v3
with:
repository: username/my-vault
token: ${{ secrets.VAULT_TOKEN }}
path: vault
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install Exocortex CLI
run: npm install -g @kitelev/exocortex-cli
- name: Create task from issue
env:
ISSUE_TITLE: ${{ github.event.issue.title }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
ISSUE_URL: ${{ github.event.issue.html_url }}
run: |
# Generate task filename
TASK_FILE="03 Knowledge/tasks/github-issue-${ISSUE_NUMBER}.md"
# Create task
exocortex command create-task "$TASK_FILE" \
--label "GitHub Issue #${ISSUE_NUMBER}: ${ISSUE_TITLE}" \
--vault vault
# Add issue URL to task body
echo "" >> "vault/$TASK_FILE"
echo "## GitHub Issue" >> "vault/$TASK_FILE"
echo "" >> "vault/$TASK_FILE"
echo "- Issue: #${ISSUE_NUMBER}" >> "vault/$TASK_FILE"
echo "- URL: ${ISSUE_URL}" >> "vault/$TASK_FILE"
- name: Commit and push
working-directory: vault
run: |
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git add .
git commit -m "Create task from issue #${ISSUE_NUMBER}"
git pushAutomatically complete tasks when PRs are merged.
.github/workflows/pr-complete-task.yml:
name: Complete Task on PR Merge
on:
pull_request:
types: [closed]
jobs:
complete-task:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Checkout vault
uses: actions/checkout@v3
with:
repository: username/my-vault
token: ${{ secrets.VAULT_TOKEN }}
path: vault
- name: Install Exocortex CLI
run: npm install -g @kitelev/exocortex-cli
- name: Extract task UID from PR
id: extract
env:
PR_BODY: ${{ github.event.pull_request.body }}
run: |
# Extract UID from PR body (format: "Task: [[task-uid]]")
TASK_UID=$(echo "$PR_BODY" | grep -oP 'Task: \[\[\K[^\]]+')
if [ -n "$TASK_UID" ]; then
echo "task_uid=$TASK_UID" >> $GITHUB_OUTPUT
fi
- name: Complete task
if: steps.extract.outputs.task_uid != ''
run: |
TASK_UID="${{ steps.extract.outputs.task_uid }}"
TASK_FILE=$(find vault -name "${TASK_UID}.md" | head -1)
if [ -n "$TASK_FILE" ]; then
# Get relative path
RELATIVE_PATH="${TASK_FILE#vault/}"
# Complete task
exocortex command complete "$RELATIVE_PATH" --vault vault
echo "✓ Completed task: $RELATIVE_PATH"
fi
- name: Commit and push
working-directory: vault
run: |
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git add .
git commit -m "Complete task: ${{ steps.extract.outputs.task_uid }}" || echo "No changes"
git push || echo "No changes to push"Send daily summary of task status.
.github/workflows/daily-status.yml:
name: Daily Task Status Report
on:
schedule:
- cron: '0 9 * * 1-5' # Weekdays at 9 AM UTC
jobs:
status-report:
runs-on: ubuntu-latest
steps:
- name: Checkout vault
uses: actions/checkout@v3
with:
repository: username/my-vault
token: ${{ secrets.VAULT_TOKEN }}
- name: Install Exocortex CLI
run: npm install -g @kitelev/exocortex-cli
- name: Generate status report
run: |
# Count tasks by status
TODO=$(grep -r "ems__Effort_status.*ToDo" . | wc -l)
DOING=$(grep -r "ems__Effort_status.*Doing" . | wc -l)
DONE=$(grep -r "ems__Effort_status.*Done" . | wc -l)
BACKLOG=$(grep -r "ems__Effort_status.*Backlog" . | wc -l)
# Create report
cat > report.md <<EOF
## Daily Task Status Report
**Date:** $(date +%Y-%m-%d)
### Summary
- **ToDo:** $TODO tasks
- **Doing:** $DOING tasks
- **Done:** $DONE tasks
- **Backlog:** $BACKLOG tasks
### Overdue Tasks
EOF
# Find overdue tasks (scheduled before today, still in ToDo)
TODAY=$(date +%Y-%m-%d)
grep -r "ems__Effort_plannedStartTimestamp" . | \
while IFS=: read file timestamp_line; do
timestamp=$(echo "$timestamp_line" | grep -oP '\d{4}-\d{2}-\d{2}')
if [[ "$timestamp" < "$TODAY" ]]; then
# Check if still ToDo
if grep -q "ems__Effort_status.*ToDo" "$file"; then
label=$(grep "exo__Asset_label:" "$file" | sed 's/.*: //')
echo "- $label ($timestamp)" >> report.md
fi
fi
done
cat report.md
- name: Send to Slack
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
run: |
REPORT=$(cat report.md)
curl -X POST "$SLACK_WEBHOOK" \
-H 'Content-Type: application/json' \
-d "{\"text\": \"$REPORT\"}".gitlab-ci.yml:
update_tasks:
stage: deploy
only:
- main
script:
- git clone https://gitlab.com/username/my-vault.git vault
- npm install -g @kitelev/exocortex-cli
# Extract task UIDs from commit messages
- |
git log --pretty=format:"%s" -1 | grep -oP 'Task: \K[a-f0-9-]+' | while read uid; do
TASK_FILE=$(find vault -name "${uid}.md" | head -1)
if [ -n "$TASK_FILE" ]; then
RELATIVE_PATH="${TASK_FILE#vault/}"
# Check current status
if grep -q "ems__Effort_status.*ToDo" "$TASK_FILE"; then
# Start task if not started
exocortex command start "$RELATIVE_PATH" --vault vault
elif grep -q "ems__Effort_status.*Doing" "$TASK_FILE"; then
# Complete task if doing
exocortex command complete "$RELATIVE_PATH" --vault vault
fi
fi
done
# Commit changes
- cd vault
- git config user.name "GitLab CI"
- git config user.email "ci@gitlab.com"
- git add .
- git commit -m "Update tasks from deployment" || echo "No changes"
- git push https://oauth2:${VAULT_TOKEN}@gitlab.com/username/my-vault.git.gitlab-ci.yml:
archive_tasks:
stage: maintenance
only:
- schedules
script:
- git clone https://gitlab.com/username/my-vault.git vault
- npm install -g @kitelev/exocortex-cli
# Archive tasks completed >30 days ago
- |
CUTOFF=$(date -d "30 days ago" +%Y-%m-%d)
grep -r "ems__Effort_resolutionTimestamp" vault/03\ Knowledge/tasks | \
while IFS=: read filepath timestamp_line; do
timestamp=$(echo "$timestamp_line" | grep -oP '\d{4}-\d{2}-\d{2}')
if [[ "$timestamp" < "$CUTOFF" ]]; then
RELATIVE_PATH="${filepath#vault/}"
exocortex command archive "$RELATIVE_PATH" --vault vault
echo "Archived: $RELATIVE_PATH"
fi
done
- cd vault
- git config user.name "GitLab CI"
- git config user.email "ci@gitlab.com"
- git add .
- git commit -m "Archive old completed tasks" || echo "No changes"
- git push https://oauth2:${VAULT_TOKEN}@gitlab.com/username/my-vault.gitGitLab Pipeline Schedule:
- Navigate to CI/CD → Schedules
- Create schedule: "Weekly archive" -
0 0 * * 0(Sundays at midnight)
Python script to create tasks from emails.
email-to-task.py:
#!/usr/bin/env python3
import imaplib
import email
import subprocess
import re
from datetime import datetime
# Email configuration
IMAP_SERVER = "imap.gmail.com"
EMAIL_ACCOUNT = "your-email@gmail.com"
EMAIL_PASSWORD = "your-app-password"
# Vault configuration
VAULT_PATH = "/path/to/vault"
TASK_DIR = "03 Knowledge/tasks"
def create_task_from_email(subject, body, sender):
"""Create Exocortex task from email"""
# Generate filename from subject
filename = re.sub(r'[^\w\s-]', '', subject.lower())
filename = re.sub(r'[-\s]+', '-', filename)
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
filepath = f"{TASK_DIR}/email-{timestamp}-{filename}.md"
# Create task
result = subprocess.run([
"exocortex", "command", "create-task", filepath,
"--label", subject,
"--vault", VAULT_PATH
], capture_output=True, text=True)
if result.returncode == 0:
# Add email content to task
task_file = f"{VAULT_PATH}/{filepath}"
with open(task_file, 'a') as f:
f.write(f"\n## Email\n\n")
f.write(f"**From:** {sender}\n\n")
f.write(f"**Date:** {datetime.now().strftime('%Y-%m-%d %H:%M')}\n\n")
f.write(f"### Message\n\n{body}\n")
print(f"✓ Created task: {filepath}")
return True
else:
print(f"✗ Failed to create task: {result.stderr}")
return False
def process_emails():
"""Check inbox and create tasks"""
# Connect to IMAP
mail = imaplib.IMAP4_SSL(IMAP_SERVER)
mail.login(EMAIL_ACCOUNT, EMAIL_PASSWORD)
mail.select("inbox")
# Search for unread emails with label "task"
status, messages = mail.search(None, '(UNSEEN SUBJECT "task")')
for num in messages[0].split():
# Fetch email
status, msg_data = mail.fetch(num, "(RFC822)")
msg = email.message_from_bytes(msg_data[0][1])
# Extract details
subject = msg["subject"]
sender = msg["from"]
body = ""
if msg.is_multipart():
for part in msg.walk():
if part.get_content_type() == "text/plain":
body = part.get_payload(decode=True).decode()
break
else:
body = msg.get_payload(decode=True).decode()
# Create task
if create_task_from_email(subject, body, sender):
# Mark as read
mail.store(num, '+FLAGS', '\\Seen')
mail.close()
mail.logout()
if __name__ == "__main__":
process_emails()Cron schedule:
# Check emails every 15 minutes
*/15 * * * * /path/to/email-to-task.py >> /var/log/email-to-task.log 2>&1Sync Jira issues to Exocortex tasks.
jira-sync.py:
#!/usr/bin/env python3
import requests
import subprocess
import json
# Jira configuration
JIRA_URL = "https://your-company.atlassian.net"
JIRA_EMAIL = "your-email@company.com"
JIRA_API_TOKEN = "your-api-token"
# Vault configuration
VAULT_PATH = "/path/to/vault"
def sync_jira_issues():
"""Sync Jira issues assigned to you"""
# Query Jira API
response = requests.get(
f"{JIRA_URL}/rest/api/3/search",
auth=(JIRA_EMAIL, JIRA_API_TOKEN),
params={
"jql": "assignee=currentUser() AND status!=Done",
"fields": "summary,status,priority,created"
}
)
issues = response.json()["issues"]
for issue in issues:
key = issue["key"]
summary = issue["fields"]["summary"]
status = issue["fields"]["status"]["name"]
# Generate task filepath
filepath = f"03 Knowledge/tasks/jira-{key.lower()}.md"
# Check if task exists
task_file = f"{VAULT_PATH}/{filepath}"
# Create if doesn't exist
result = subprocess.run([
"find", VAULT_PATH, "-name", f"jira-{key.lower()}.md"
], capture_output=True, text=True)
if not result.stdout.strip():
# Create new task
subprocess.run([
"exocortex", "command", "create-task", filepath,
"--label", f"{key}: {summary}",
"--vault", VAULT_PATH
])
# Add Jira metadata
with open(task_file, 'a') as f:
f.write(f"\n## Jira\n\n")
f.write(f"- **Key:** {key}\n")
f.write(f"- **URL:** {JIRA_URL}/browse/{key}\n")
f.write(f"- **Status:** {status}\n")
print(f"✓ Created: {key}")
else:
# Update existing task based on Jira status
if status == "In Progress":
subprocess.run([
"exocortex", "command", "start", filepath,
"--vault", VAULT_PATH
])
print(f"✓ Started: {key}")
elif status == "Done":
subprocess.run([
"exocortex", "command", "complete", filepath,
"--vault", VAULT_PATH
])
print(f"✓ Completed: {key}")
if __name__ == "__main__":
sync_jira_issues()todoist-sync.sh:
#!/bin/bash
# Sync Todoist tasks to Exocortex
VAULT_PATH="/path/to/vault"
TODOIST_TOKEN="your-todoist-api-token"
# Get active Todoist tasks
curl -X GET \
"https://api.todoist.com/rest/v2/tasks" \
-H "Authorization: Bearer $TODOIST_TOKEN" \
| jq -r '.[] | "\(.id),\(.content),\(.due.date // "no-date")"' \
| while IFS=',' read id content due_date; do
# Generate task filename
filepath="03 Knowledge/tasks/todoist-$id.md"
# Check if task exists
if [ ! -f "$VAULT_PATH/$filepath" ]; then
# Create task
exocortex command create-task "$filepath" \
--label "$content" \
--vault "$VAULT_PATH"
# Schedule if has due date
if [ "$due_date" != "no-date" ]; then
exocortex command schedule "$filepath" \
--date "$due_date" \
--vault "$VAULT_PATH"
fi
echo "✓ Synced from Todoist: $content"
fi
donenotion-sync.js:
#!/usr/bin/env node
const { Client } = require("@notionhq/client");
const { exec } = require("child_process");
const util = require("util");
const execAsync = util.promisify(exec);
const notion = new Client({ auth: process.env.NOTION_TOKEN });
const databaseId = process.env.NOTION_DATABASE_ID;
const vaultPath = process.env.VAULT_PATH;
async function syncNotionTasks() {
// Query Notion database
const response = await notion.databases.query({
database_id: databaseId,
filter: {
property: "Status",
select: {
does_not_equal: "Done"
}
}
});
for (const page of response.results) {
const title = page.properties.Name.title[0].plain_text;
const notionId = page.id;
const filepath = `03 Knowledge/tasks/notion-${notionId}.md`;
// Check if task exists
const { stdout } = await execAsync(
`find "${vaultPath}" -name "notion-${notionId}.md"`
);
if (!stdout.trim()) {
// Create task
await execAsync(
`exocortex command create-task "${filepath}" --label "${title}" --vault "${vaultPath}"`
);
console.log(`✓ Synced from Notion: ${title}`);
}
}
}
syncNotionTasks().catch(console.error);Package.json:
{
"dependencies": {
"@notionhq/client": "^2.2.0"
}
}Run:
export NOTION_TOKEN="your-notion-integration-token"
export NOTION_DATABASE_ID="your-database-id"
export VAULT_PATH="/path/to/vault"
node notion-sync.jswebhook-server.js:
#!/usr/bin/env node
const express = require("express");
const { exec } = require("child_process");
const util = require("util");
const execAsync = util.promisify(exec);
const app = express();
app.use(express.json());
const VAULT_PATH = process.env.VAULT_PATH || "/path/to/vault";
// Webhook endpoint for task creation
app.post("/webhook/create-task", async (req, res) => {
try {
const { label, area, date } = req.body;
if (!label) {
return res.status(400).json({ error: "Label required" });
}
// Generate filename
const timestamp = Date.now();
const filename = label.toLowerCase().replace(/\s+/g, "-");
const filepath = `03 Knowledge/tasks/webhook-${timestamp}-${filename}.md`;
// Create task
let command = `exocortex command create-task "${filepath}" --label "${label}" --vault "${VAULT_PATH}"`;
if (area) {
command += ` --area "${area}"`;
}
await execAsync(command);
// Schedule if date provided
if (date) {
await execAsync(
`exocortex command schedule "${filepath}" --date "${date}" --vault "${VAULT_PATH}"`
);
}
res.json({ success: true, filepath });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Webhook endpoint for status updates
app.post("/webhook/update-status", async (req, res) => {
try {
const { uid, status } = req.body;
// Find task file
const { stdout } = await execAsync(
`find "${VAULT_PATH}" -name "${uid}.md"`
);
if (!stdout.trim()) {
return res.status(404).json({ error: "Task not found" });
}
const filepath = stdout.trim().replace(`${VAULT_PATH}/`, "");
// Update status
const commandMap = {
start: "start",
complete: "complete",
todo: "move-to-todo",
backlog: "move-to-backlog"
};
const command = commandMap[status];
if (!command) {
return res.status(400).json({ error: "Invalid status" });
}
await execAsync(
`exocortex command ${command} "${filepath}" --vault "${VAULT_PATH}"`
);
res.json({ success: true });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.listen(3000, () => {
console.log("Webhook server listening on port 3000");
});Start server:
export VAULT_PATH="/path/to/vault"
node webhook-server.jsUsage:
# Create task via webhook
curl -X POST http://localhost:3000/webhook/create-task \
-H "Content-Type: application/json" \
-d '{"label": "New task from webhook", "date": "2025-11-25"}'
# Update task status
curl -X POST http://localhost:3000/webhook/update-status \
-H "Content-Type: application/json" \
-d '{"uid": "task-uid-123", "status": "start"}'crontab:
# Daily cleanup at 2 AM
0 2 * * * /path/to/scripts/daily-cleanup.sh >> /var/log/exocortex-cleanup.log 2>&1
# Weekly archive on Sundays at 3 AM
0 3 * * 0 /path/to/scripts/weekly-archive.sh >> /var/log/exocortex-archive.log 2>&1
# Sync external tools every 30 minutes
*/30 * * * * /path/to/scripts/external-sync.sh >> /var/log/exocortex-sync.log 2>&1
# Generate daily report at 9 AM on weekdays
0 9 * * 1-5 /path/to/scripts/daily-report.sh >> /var/log/exocortex-report.log 2>&1- Command Reference - Complete command syntax
- Scripting Patterns - Bash scripting examples
- Troubleshooting - Debugging integration issues