Skill: olegantonov/limesurvey-openclaw-skill (version 1.2.2)

--- name: limesurvey description: "Automate LimeSurvey operations via RemoteControl 2 API. Use when: (1) managing surveys, questions, groups, or participants, (2) exporting responses or statistics, (3) sending invitations or reminders, (4) importing surveys or questions, (5) batch operations on survey data. Supports JSON-RPC API with full CRUD operations for surveys, participants, responses, questions, and groups. **REQUIRED ENVIRONMENT VARIABLES:** LIMESURVEY_URL (your RemoteControl endpoint), LIMESURVEY_USER, LIMESURVEY_PASSWORD (use least-privilege service account, never full admin credentials)." metadata: openclaw: requires: env: - LIMESURVEY_URL - LIMESURVEY_USER - LIMESURVEY_PASSWORD credentials: - name: limesurvey-service-account description: "LimeSurvey RemoteControl service account (least-privilege recommended)" env: LIMESURVEY_USER,LIMESURVEY_PASSWORD homepage: "https://github.com/olegantonov/limesurvey-openclaw-skill" version: "1.2.1" author: "Daniel Marques" license: "MIT" --- # LimeSurvey RemoteControl 2 API Automate LimeSurvey survey management via the RemoteControl 2 JSON-RPC API. ## Prerequisites **Required Environment Variables:** - `LIMESURVEY_URL` — Full URL to RemoteControl endpoint (e.g., `https://survey.example.com/index.php/admin/remotecontrol`) - `LIMESURVEY_USER` — Service account username - `LIMESURVEY_PASSWORD` — Service account password **Security Recommendations:** 1. **Use a least-privilege service account**, not your full admin credentials 2. Create a dedicated LimeSurvey user with minimal required permissions (survey management only, no system administration) 3. Use a strong, unique password; rotate periodically after initial use 4. Never commit credentials to version control — use environment variables or secure vaults only 5. Verify the RemoteControl API is accessible only from trusted networks **Setup:** ```bash export LIMESURVEY_URL='https://survey.example.com/index.php/admin/remotecontrol' export LIMESURVEY_USER='service_account_username' export LIMESURVEY_PASSWORD='secure_service_password' ``` ## Quick Start ### Setup Set environment variables: ```bash export LIMESURVEY_URL='https://survey.example.com/index.php/admin/remotecontrol' export LIMESURVEY_USER='admin' export LIMESURVEY_PASSWORD='your_password' ``` ### CLI Usage ```bash # List all surveys python3 scripts/limesurvey.py list-surveys # Export responses python3 scripts/limesurvey.py export-responses 123456 --format csv -o responses.csv # List participants python3 scripts/limesurvey.py list-participants 123456 --limit 100 # Add participants from JSON python3 scripts/limesurvey.py add-participants 123456 --file participants.json # Send invitations python3 scripts/limesurvey.py invite-participants 123456 # Activate survey python3 scripts/limesurvey.py activate-survey 123456 # Get survey statistics python3 scripts/limesurvey.py get-summary 123456 ``` ### Python API Usage ```python from scripts.limesurvey_client import LimeSurveySession url = 'https://survey.example.com/index.php/admin/remotecontrol' with LimeSurveySession(url, 'admin', 'password') as client: # List surveys surveys = client.call('list_surveys', client.session_key) # Export responses encoded = client.call('export_responses', client.session_key, survey_id, 'csv') csv_data = client.decode_base64(encoded) ``` ## Common Workflows ### Survey Management **List all surveys:** ```python surveys = client.call('list_surveys', client.session_key) ``` **Activate a survey:** ```python result = client.call('activate_survey', client.session_key, survey_id) ``` **Get survey statistics:** ```python stats = client.call('get_summary', client.session_key, survey_id, 'all') # Returns: completed_responses, incomplete_responses, token_count, etc. ``` **Copy a survey:** ```python result = client.call('copy_survey', client.session_key, source_id, "New Survey Name") new_id = result['newsid'] ``` ### Participant Management **Initialize participant table:** ```python # Create table with custom attributes client.call('activate_tokens', client.session_key, survey_id, [1, 2, 3]) ``` **Add participants:** ```python participants = [ {"email": "[REDACTED_SECRET]", "firstname": "John", "lastname": "Doe"}, {"email": "[REDACTED_SECRET]", "firstname": "Jane", "lastname": "Smith"} ] result = client.call('add_participants', client.session_key, survey_id, participants, True) ``` **Send invitations:** ```python # Send to specific tokens token_ids = [1, 2, 3] result = client.call('invite_participants', client.session_key, survey_id, token_ids) # Or send to all pending result = client.call('invite_participants', client.session_key, survey_id, None) ``` **List participants with filters:** ```python # Find unused tokens participants = client.call('list_participants', client.session_key, survey_id, 0, 100, True) # Find by custom attribute participants = client.call('list_participants', client.session_key, survey_id, 0, 1000, False, False, {'attribute_1': 'ACME Corp'}) ``` ### Response Operations **Export all responses:** ```python encoded = client.call('export_responses', client.session_key, survey_id, 'csv') csv_data = client.decode_base64(encoded) with open('responses.csv', 'w') as f: f.write(csv_data) ``` **Export only completed responses:** ```python encoded = client.call('export_responses', client.session_key, survey_id, 'csv', None, 'complete') ``` **Export by token:** ```python encoded = client.call('export_responses_by_token', client.session_key, survey_id, 'json', ['token1', 'token2']) ``` **Add a response programmatically:** ```python response_data = { 'G1Q1': 'Answer text', 'G1Q2': '3', # Multiple choice value 'token': 'xyz123' } response_id = client.call('add_response', client.session_key, survey_id, response_data) ``` ### Question & Group Operations **List groups:** ```python groups = client.call('list_groups', client.session_key, survey_id) ``` **Add a question group:** ```python group_id = client.call('add_group', client.session_key, survey_id, 'Demographics', 'Basic information') ``` **List questions:** ```python # All questions in survey questions = client.call('list_questions', client.session_key, survey_id) # Questions in specific group questions = client.call('list_questions', client.session_key, survey_id, group_id) ``` **Get question details:** ```python props = client.call('get_question_properties', client.session_key, question_id, ['question', 'type', 'mandatory']) ``` ### Export & Reporting **Generate statistics PDF:** ```python encoded = client.call('export_statistics', client.session_key, survey_id, 'pdf', None, '1') # With graphs import base64 pdf_data = base64.b64decode(encoded) with open('stats.pdf', 'wb') as f: f.write(pdf_data) ``` **Get submission timeline:** ```python timeline = client.call('export_timeline', client.session_key, survey_id, 'day', '2024-01-01', '2024-12-31') for entry in timeline: print(f"{entry['period']}: {entry['count']} submissions") ``` **Get survey fieldmap:** ```python fieldmap = client.call('get_fieldmap', client.session_key, survey_id) # Maps question codes to metadata ``` ## Error Handling All API functions return status objects on error: ```python result = client.call('activate_survey', client.session_key, survey_id) if isinstance(result, dict) and 'status' in result: print(f"Error: {result['status']}") # Common errors: # - Invalid session key # - No permission # - Invalid survey ID # - Survey already active else: print("Success!") ``` Use try/except for connection errors: ```python from scripts.limesurvey_client import LimeSurveyError try: with LimeSurveySession(url, username, password) as client: result = client.call('list_surveys', client.session_key) except LimeSurveyError as e: print(f"API error: {e}") ``` ## Reference Documentation - **[API Reference](references/api_reference.md)** - Complete function reference with parameters and return values - **[Examples](references/examples.md)** - Practical code examples for common tasks ## Available Functions ### Session - `get_session_key(username, password)` - Create session - `release_session_key(session_key)` - Close session ### Surveys - `list_surveys(session_key)` - List all surveys - `get_survey_properties(session_key, survey_id, properties)` - Get properties - `set_survey_properties(session_key, survey_id, properties)` - Update properties - `activate_survey(session_key, survey_id)` - Activate survey - `delete_survey(session_key, survey_id)` - Delete survey - `copy_survey(session_key, source_id, new_name)` - Duplicate survey - `import_survey(session_key, data, type)` - Import from file - `get_summary(session_key, survey_id, stat)` - Get statistics ### Participants - `activate_tokens(session_key, survey_id, attributes)` - Initialize table - `list_participants(session_key, survey_id, start, limit, unused, attributes, conditions)` - List participants - `add_participants(session_key, survey_id, data, create_token)` - Add participants - `delete_participants(session_key, survey_id, token_ids)` - Delete participants - `get_participant_properties(session_key, survey_id, query, properties)` - Get properties - `set_participant_properties(session_key, survey_id, query, properties)` - Update properties - `invite_participants(session_key, survey_id, token_ids)` - Send invitations - `remind_participants(session_key, survey_id, token_ids)` - Send reminders ### Responses - `export_responses(session_key, survey_id, format, ...)` - Export responses - `export_responses_by_token(session_key, survey_id, format, tokens)` - Export by token - `add_response(session_key, survey_id, data)` - Add response - `update_response(session_key, survey_id, data)` - Update response - `delete_response(session_key, survey_id, response_id)` - Delete response - `get_response_ids(session_key, survey_id, token)` - Find response IDs ### Questions & Groups - `list_groups(session_key, survey_id)` - List groups - `add_group(session_key, survey_id, title, description)` - Create group - `delete_group(session_key, survey_id, group_id)` - Delete group - `get_group_properties(session_key, group_id, properties)` - Get properties - `set_group_properties(session_key, group_id, properties)` - Update properties - `list_questions(session_key, survey_id, group_id)` - List questions - `get_question_properties(session_key, question_id, properties)` - Get properties - `set_question_properties(session_key, question_id, properties)` - Update properties - `import_question(session_key, survey_id, group_id, data, type)` - Import question - `delete_question(session_key, question_id)` - Delete question ### Export & Reporting - `export_statistics(session_key, survey_id, doc_type, language, graph)` - Export statistics - `export_timeline(session_key, survey_id, type, start, end)` - Submission timeline - `get_fieldmap(session_key, survey_id)` - Get question codes ## Notes - **Session management**: Always close sessions with `release_session_key` or use `LimeSurveySession` context manager - **Base64 encoding**: Export functions return base64-encoded data — use `client.decode_base64()` or `base64.b64decode()` - **Permissions**: API calls respect user permissions — same as logging into admin interface - **Rate limiting**: LimeSurvey has brute-force protection on authentication - **JSON-RPC**: Content-Type must be `application/json` (handled automatically by client) ## Full API Documentation Official LimeSurvey API: https://api.limesurvey.org/classes/remotecontrol-handle.html Manual: https://www.limesurvey.org/manual/RemoteControl_2_API

path: README.md
content: # LimeSurvey Skill for OpenClaw

> **Automate LimeSurvey operations via RemoteControl 2 API**

Complete OpenClaw skill for managing LimeSurvey surveys, participants, responses, and more through the official JSON-RPC API.

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![OpenClaw](https://img.shields.io/badge/OpenClaw-Skill-blue)](https://openclaw.ai)
[![LimeSurvey API](https://img.shields.io/badge/LimeSurvey-RemoteControl%202-green)](https://api.limesurvey.org/)

## Features

✅ **Survey Management**
- List, activate, copy, delete, import surveys
- Get statistics and summaries

✅ **Participant Management**
- Batch add participants (CSV/JSON)
- Send invitations and reminders
- Filter by custom attributes

✅ **Response Export**
- Formats: CSV, Excel, PDF, JSON
- Filters: complete/incomplete, by token, by period
- Automatic Base64 decoding

✅ **Questions & Groups**
- Full CRUD operations
- Import from .lsq files

✅ **Reports & Analytics**
- Statistics in PDF/Excel with charts
- Submission timelines
- Fieldmap for analysis

## Quick Start

### Installation

For OpenClaw users:

1. Download the skill:
   ```bash
   clawhub install limesurvey
   ```

2. Configure credentials:
   ```bash
   export LIMESURVEY_URL='https://survey.example.com/index.php/admin/remotecontrol'
   export LIMESURVEY_USER='admin'
   export LIMESURVEY_PASSWORD='password'
   ```

### CLI Usage

```bash
# List all surveys
python3 skills/limesurvey/scripts/limesurvey.py list-surveys

# Export responses
python3 skills/limesurvey/scripts/limesurvey.py export-responses 123456 --format csv -o responses.csv

# List participants
python3 skills/limesurvey/scripts/limesurvey.py list-participants 123456 --limit 100

# Send invitations
python3 skills/limesurvey/scripts/limesurvey.py invite-participants 123456

# Activate survey
python3 skills/limesurvey/scripts/limesurvey.py activate-survey 123456

# Get summary statistics
python3 skills/limesurvey/scripts/limesurvey.py get-summary 123456
```

### Python API

```python
from scripts.limesurvey_client import LimeSurveySession

url = 'https://survey.example.com/index.php/admin/remotecontrol'

with LimeSurveySession(url, 'admin', 'password') as client:
    # List surveys
    surveys = client.call('list_surveys', client.session_key)
    
    # Export responses
    encoded = client.call('export_responses', client.session_key, 
                         survey_id, 'csv', None, 'complete')
    csv_data = client.decode_base64(encoded)
    
    # Add participants
    participants = [
        {"email": "[REDACTED_SECRET]", "firstname": "John", "lastname": "Doe"}
    ]
    result = client.call('add_participants', client.session_key, 
                        survey_id, participants)
```

## Documentation

- **[SKILL.md](SKILL.md)** - Main guide with common workflows
- **[API Reference](references/api_reference.md)** - Complete function reference (50+ functions)
- **[Examples](references/examples.md)** - Practical code examples

## Structure

```
limesurvey/
├── SKILL.md                      # Main skill guide
├── README.md                     # This file
├── scripts/
│   ├── limesurvey_client.py     # JSON-RPC Python client
│   └── limesurvey.py            # CLI executable
└── references/
    ├── api_reference.md          # Complete API reference
    └── examples.md               # Practical examples
```

## Requirements

- Python 3.6+
- LimeSurvey instance with RemoteControl 2 API enabled
- Valid LimeSurvey credentials

**No external dependencies** - Uses only Python standard library.

## Common Workflows

### Batch Import Participants

```python
import csv

participants = []
with open('participants.csv', 'r') as f:
    reader = csv.DictReader(f)
    for row in reader:
        participants.append({
            'email': row['email'],
            'firstname': row['firstname'],
            'lastname': row['lastname']
        })

with LimeSurveySession(url, username, password) as client:
    result = client.call('add_participants', client.session_key, 
                        survey_id, participants, True)
    print(f"Added {len(result)} participants")
```

### Daily Response Export

```python
with LimeSurveySession(url, username, password) as client:
    encoded = client.call('export_responses', client.session_key,
                         survey_id, 'csv', None, 'complete')
    
    csv_data = client.decode_base64(encoded)
    
    with open(f'responses_{date.today()}.csv', 'w') as f:
        f.write(csv_data)
```

### Send Reminders to Incomplete

```python
with LimeSurveySession(url, username, password) as client:
    # Get incomplete tokens
    participants = client.call('list_participants', client.session_key,
                              survey_id, 0, 1000, False, False,
                              {'completed': 'N'})
    
    token_ids = [p['tid'] for p in participants]
    
    # Send reminders
    result = client.call('remind_participants', client.session_key, 
                        survey_id, token_ids)
```

## API Coverage

This skill covers **50+ API functions** including:

- Session: `get_session_key`, `release_session_key`
- Surveys: `list_surveys`, `activate_survey`, `copy_survey`, `import_survey`, `get_summary`
- Participants: `add_participants`, `list_participants`, `invite_participants`, `remind_participants`
- Responses: `export_responses`, `add_response`, `update_response`, `delete_response`
- Questions/Groups: `list_questions`, `add_group`, `import_question`
- Export: `export_statistics`, `export_timeline`, `get_fieldmap`

See [API Reference](references/api_reference.md) for complete documentation.

## Error Handling

```python
from scripts.limesurvey_client import LimeSurveyError

try:
    with LimeSurveySession(url, username, password) as client:
        result = client.call('activate_survey', client.session_key, survey_id)
        
        if isinstance(result, dict) and 'status' in result:
            print(f"Error: {result['status']}")
        else:
            print("Success!")
            
except LimeSurveyError as e:
    print(f"API error: {e}")
```

## About OpenClaw

[OpenClaw](https://openclaw.ai) is an AI agent framework with modular skills. This skill enables AI agents to automate LimeSurvey operations.

## Resources

- [Official LimeSurvey API Documentation](https://api.limesurvey.org/classes/remotecontrol-handle.html)
- [RemoteControl 2 Manual](https://www.limesurvey.org/manual/RemoteControl_2_API)
- [OpenClaw Documentation](https://docs.openclaw.ai)
- [ClawHub - Skill Repository](https://clawhub.com)

## Contributing

Contributions welcome! Please open an issue or PR.

## License

MIT License - See LICENSE file for details

## Author

Created by **Daniel Marques** ([@olegantonov](https://github.com/olegantonov))

---

**Made for survey automation**


path: references/api_reference.md
content: # LimeSurvey RemoteControl 2 API Reference

Quick reference for the most commonly used API functions.

## Table of Contents

- Session Management
- Survey Operations
- Participant Management
- Response Management
- Question & Group Operations
- Export Operations

## Session Management

### get_session_key(username, password, plugin='Authdb')

Create a new session key. **Mandatory for all API calls.**

**Returns:** Session key string

**Example:**
```python
session_key = client.get_session_key('admin', 'password')
```

### release_session_key(session_key)

Close the RPC session.

**Returns:** 'OK'

---

## Survey Operations

### list_surveys(session_key, username=None)

List surveys accessible to the user.

**Returns:** Array of survey objects with `sid`, `surveyls_title`, `active`, `startdate`, `expires`

### get_survey_properties(session_key, survey_id, properties=None)

Get survey properties. If `properties` is None, returns all.

**Available properties:** sid, active, admin, adminname, adminemail, language, format, template, etc.

### set_survey_properties(session_key, survey_id, properties)

Update survey properties.

**Parameters:**
- `properties`: Dictionary with property names and new values

### activate_survey(session_key, survey_id, settings=[])

Activate a survey (create response table).

**Returns:** Status object

### delete_survey(session_key, survey_id)

Permanently delete a survey.

**Returns:** Status object

### copy_survey(session_key, source_survey_id, new_name, dest_survey_id=None)

Duplicate a survey.

**Returns:** New survey ID in `newsid` key

### import_survey(session_key, import_data, import_type, new_name=None, dest_id=None)

Import survey from BASE64-encoded file.

**Supported formats:** lss, csv, txt, lsa

**Example:**
```python
with open('survey.lss', 'rb') as f:
    encoded = base64.b64encode(f.read()).decode()
new_id = client.call('import_survey', session_key, encoded, 'lss', 'My Survey')
```

### get_summary(session_key, survey_id, stat_name='all')

Get survey statistics.

**Available stats:**
- `completed_responses` - Number of completed responses
- `incomplete_responses` - Number of incomplete responses
- `full_responses` - Total responses
- `token_count` - Total tokens
- `token_sent` - Tokens with sent invitations
- `token_completed` - Tokens with completed responses
- `token_opted_out` - Opted-out tokens

---

## Participant Management

### activate_tokens(session_key, survey_id, attribute_fields=[])

Initialize the participant table for a survey.

**Parameters:**
- `attribute_fields`: Array of integers for additional attribute fields

### list_participants(session_key, survey_id, start=0, limit=10, unused=False, attributes=False, conditions=[])

List survey participants.

**Parameters:**
- `start`: Start index (pagination)
- `limit`: Max results to return
- `unused`: If True, return only unused tokens
- `attributes`: Array of additional attributes to include
- `conditions`: Filter conditions (see examples below)

**Condition examples:**
```python
# Simple equality
conditions = {'email': '[REDACTED_SECRET]'}

# Operators
conditions = {'validuntil': ['>', '2024-01-01 00:00:00']}

# IN operator
conditions = {'tid': ['IN', '1', '3', '26']}
```

**Returns:** Array with `tid`, `token`, `participant_info` (firstname, lastname, email)

### add_participants(session_key, survey_id, participant_data, create_token=True)

Add participants to survey.

**Parameters:**
- `participant_data`: Array of participant objects
- `create_token`: Auto-generate token if True

**Participant object:**
```python
{
    "email": "[REDACTED_SECRET]",
    "firstname": "John",
    "lastname": "Doe",
    "attribute_1": "custom_value"  # Optional custom attributes
}
```

**Returns:** Inserted data including token IDs and token strings

### delete_participants(session_key, survey_id, token_ids)

Delete multiple participants.

**Parameters:**
- `token_ids`: Array of token IDs to delete

### get_participant_properties(session_key, survey_id, query, properties=None)

Get participant details.

**Parameters:**
- `query`: Token ID (int) or dict with search criteria
- `properties`: Array of properties to fetch (None = all)

### set_participant_properties(session_key, survey_id, token_query, properties)

Update participant properties.

**Example:**
```python
client.call('set_participant_properties', session_key, survey_id, 
            {'tid': 123}, 
            {'email': '[REDACTED_SECRET]', 'validuntil': '2025-12-31'})
```

### invite_participants(session_key, survey_id, token_ids=None, email=True, continue_on_error=False)

Send invitation emails.

**Parameters:**
- `token_ids`: Array of token IDs (None = all pending)
- `email`: True for pending invites, False to resend
- `continue_on_error`: Don't stop on first invalid token

### remind_participants(session_key, survey_id, token_ids=None)

Send reminder emails to participants who haven't completed.

---

## Response Management

### export_responses(session_key, survey_id, format, language=None, completion='all', heading='code', response='short', from_id=None, to_id=None, fields=None)

Export survey responses as BASE64-encoded file.

**Parameters:**
- `format`: csv, pdf, xls, doc, json
- `completion`: all, complete, incomplete
- `heading`: code, full, abbreviated
- `response`: short, long
- `from_id`, `to_id`: Response ID range
- `fields`: Array of field names to export

**Returns:** Base64-encoded string (decode before use)

### export_responses_by_token(session_key, survey_id, format, tokens, language=None, ...)

Export responses for specific tokens.

**Parameters:**
- `tokens`: String or array of token values

### add_response(session_key, survey_id, response_data)

Add a response programmatically.

**Parameters:**
- `response_data`: Dict with question codes as keys

**Example:**
```python
response = {
    'G01Q01': 'Answer text',
    'G01Q02': '3',  # Multiple choice value
    'token': 'xyz123'  # Optional
}
result = client.call('add_response', session_key, survey_id, response)
```

**Returns:** Response ID

### update_response(session_key, survey_id, response_data)

Update an existing response.

**Note:** Must include response `id` in `response_data`

### delete_response(session_key, survey_id, response_id)

Delete a response.

### get_response_ids(session_key, survey_id, token)

Find response IDs for a given token.

**Returns:** Array of response IDs

---

## Question & Group Operations

### list_groups(session_key, survey_id, language=None)

List all question groups in a survey.

**Returns:** Array with `gid`, `group_name`, `group_order`, `description`

### add_group(session_key, survey_id, title, description='')

Create a new question group.

**Returns:** New group ID

### delete_group(session_key, survey_id, group_id)

Delete a question group and all its questions.

### get_group_properties(session_key, group_id, settings=None, language=None)

Get group properties.

### set_group_properties(session_key, group_id, properties)

Update group properties.

**Example:**
```python
client.call('set_group_properties', session_key, group_id, {
    'group_name': 'Demographics',
    'description': 'Basic information'
})
```

### list_questions(session_key, survey_id, group_id=None, language=None)

List questions in a survey or group.

**Returns:** Array with `qid`, `title`, `question`, `type`, `gid`, `question_order`

### get_question_properties(session_key, question_id, settings=None, language=None)

Get question properties.

**Additional properties:** `available_answers`, `subquestions`, `attributes`, `defaultvalue`

### import_question(session_key, survey_id, group_id, import_data, format, mandatory='N', title=None, text=None, help=None)

Import a question from BASE64-encoded .lsq file.

### delete_question(session_key, question_id)

Delete a question.

---

## Export Operations

### export_statistics(session_key, survey_id, doc_type='pdf', language=None, graph='0', group_ids=None)

Export statistics report.

**Parameters:**
- `doc_type`: pdf, xls, html
- `graph`: '0' or '1' to include graphs
- `group_ids`: Array of group IDs to include

**Returns:** Base64-encoded file

### export_timeline(session_key, survey_id, type, start, end)

Export submission timeline.

**Parameters:**
- `type`: 'day' or 'hour'
- `start`: Date string (YYYY-MM-DD)
- `end`: Date string

**Returns:** Array with count and period

### get_fieldmap(session_key, survey_id, language=None)

Get survey fieldmap (question codes and metadata).

**Returns:** Dictionary mapping field names to question metadata

---

## Common Error Handling

All functions return status arrays on error:

```python
{'status': 'Invalid session key'}
{'status': 'No permission'}
{'status': 'Invalid survey ID'}
```

Always check for `status` key in responses.

---

## Complete API Documentation

Full API reference: https://api.limesurvey.org/classes/remotecontrol-handle.html


path: references/examples.md
content: # LimeSurvey Common Examples

Practical code examples for common tasks.

## Setup

All examples assume the client is already initialized:

```python
from limesurvey_client import LimeSurveySession

url = 'https://survey.example.com/index.php/admin/remotecontrol'
username = 'admin'
[REDACTED_SECRET]'

with LimeSurveySession(url, username, password) as client:
    # Your code here
    pass
```

---

## Survey Management

### List all surveys

```python
with LimeSurveySession(url, username, password) as client:
    surveys = client.call('list_surveys', client.session_key)
    
    for survey in surveys:
        print(f"{survey['sid']}: {survey['surveyls_title']} (Active: {survey['active']})")
```

### Activate a survey

```python
with LimeSurveySession(url, username, password) as client:
    result = client.call('activate_survey', client.session_key, survey_id)
    
    if isinstance(result, dict) and 'status' in result:
        print(f"Activation result: {result['status']}")
    else:
        print("Survey activated successfully")
```

### Get survey statistics

```python
with LimeSurveySession(url, username, password) as client:
    stats = client.call('get_summary', client.session_key, survey_id, 'all')
    
    print(f"Completed: {stats.get('completed_responses')}")
    print(f"Incomplete: {stats.get('incomplete_responses')}")
    print(f"Total tokens: {stats.get('token_count')}")
```

### Copy a survey

```python
with LimeSurveySession(url, username, password) as client:
    result = client.call('copy_survey', client.session_key, 
                        source_survey_id, 
                        "Copy of Survey", 
                        new_survey_id)  # Optional
    
    new_id = result.get('newsid')
    print(f"New survey ID: {new_id}")
```

---

## Participant Management

### Initialize participant table

```python
with LimeSurveySession(url, username, password) as client:
    # Create table with 5 custom attributes
    result = client.call('activate_tokens', client.session_key, survey_id, [1, 2, 3, 4, 5])
    print(result)  # {'status': 'OK'}
```

### Add participants from CSV data

```python
import csv

participants = []
with open('participants.csv', 'r') as f:
    reader = csv.DictReader(f)
    for row in reader:
        participants.append({
            'email': row['email'],
            'firstname': row['firstname'],
            'lastname': row['lastname'],
            'attribute_1': row.get('company', '')
        })

with LimeSurveySession(url, username, password) as client:
    result = client.call('add_participants', client.session_key, 
                        survey_id, participants, True)
    
    print(f"Added {len(result)} participants")
    for p in result:
        print(f"  {p['email']}: {p['token']}")
```

### Send invitations to specific participants

```python
with LimeSurveySession(url, username, password) as client:
    # Get participants without sent invitations
    participants = client.call('list_participants', client.session_key, 
                              survey_id, 0, 1000, False, False,
                              {'sent': 'N'})
    
    token_ids = [p['tid'] for p in participants]
    
    # Send invitations
    result = client.call('invite_participants', client.session_key, 
                        survey_id, token_ids)
    
    for r in result:
        print(f"Token {r['tid']}: {r['status']}")
```

### Find participants by custom attribute

```python
with LimeSurveySession(url, username, password) as client:
    # Find all participants from "ACME Corp"
    participants = client.call('list_participants', client.session_key,
                              survey_id, 0, 1000, False, ['attribute_1'],
                              {'attribute_1': 'ACME Corp'})
    
    for p in participants:
        info = p['participant_info']
        print(f"{info['firstname']} {info['lastname']}: {info['email']}")
```

### Update participant email

```python
with LimeSurveySession(url, username, password) as client:
    result = client.call('set_participant_properties', 
                        client.session_key, survey_id,
                        {'tid': 123},  # Query by token ID
                        {'email': '[REDACTED_SECRET]'})
    
    print(result)
```

---

## Response Management

### Export all responses as CSV

```python
with LimeSurveySession(url, username, password) as client:
    # Get base64-encoded CSV
    encoded = client.call('export_responses', client.session_key, 
                         survey_id, 'csv')
    
    # Decode and save
    csv_data = client.decode_base64(encoded)
    
    with open('responses.csv', 'w', encoding='utf-8') as f:
        f.write(csv_data)
    
    print("Exported to responses.csv")
```

### Export only completed responses as Excel

```python
with LimeSurveySession(url, username, password) as client:
    encoded = client.call('export_responses', client.session_key,
                         survey_id, 'xls', None, 'complete')
    
    import base64
    xls_data = base64.b64decode(encoded)
    
    with open('completed.xls', 'wb') as f:
        f.write(xls_data)
```

### Export responses for specific participants

```python
with LimeSurveySession(url, username, password) as client:
    tokens = ['abc123', 'def456', 'ghi789']
    
    encoded = client.call('export_responses_by_token',
                         client.session_key, survey_id, 'csv', tokens)
    
    csv_data = client.decode_base64(encoded)
    print(csv_data)
```

### Add a response programmatically

```python
with LimeSurveySession(url, username, password) as client:
    # Get fieldmap to know question codes
    fieldmap = client.call('get_fieldmap', client.session_key, survey_id)
    
    # Build response
    response_data = {
        'G1Q1': 'John',           # Text answer
        'G1Q2': 'Doe',
        'G2Q1': '3',              # Multiple choice (value)
        'G2Q2A1': 'Y',            # Array question subquestion
        'token': 'xyz123'         # Optional: link to participant
    }
    
    response_id = client.call('add_response', client.session_key, 
                             survey_id, response_data)
    
    print(f"Created response ID: {response_id}")
```

### Get all responses for a token

```python
with LimeSurveySession(url, username, password) as client:
    [REDACTED_SECRET]'
    
    # Find response IDs
    response_ids = client.call('get_response_ids', client.session_key, 
                              survey_id, token)
    
    if response_ids:
        print(f"Token {token} has {len(response_ids)} response(s)")
        
        # Export just these responses
        encoded = client.call('export_responses', client.session_key,
                            survey_id, 'json', None, 'all', 'code', 'short',
                            min(response_ids), max(response_ids))
        
        json_data = client.decode_base64(encoded)
        print(json_data)
```

---

## Question & Group Management

### List all questions in a survey

```python
with LimeSurveySession(url, username, password) as client:
    questions = client.call('list_questions', client.session_key, survey_id)
    
    for q in questions:
        print(f"[{q['title']}] {q['question']} (Type: {q['type']})")
```

### Create a new question group

```python
with LimeSurveySession(url, username, password) as client:
    group_id = client.call('add_group', client.session_key, survey_id,
                          'Demographics', 
                          'Basic participant information')
    
    print(f"Created group ID: {group_id}")
```

### Get question details including subquestions

```python
with LimeSurveySession(url, username, password) as client:
    props = client.call('get_question_properties', client.session_key,
                       question_id, 
                       ['question', 'type', 'subquestions', 'answeroptions'])
    
    print(f"Question: {props['question']}")
    print(f"Type: {props['type']}")
    
    if props.get('subquestions'):
        print("Subquestions:")
        for sq in props['subquestions'].values():
            print(f"  - {sq['title']}: {sq['question']}")
```

### Update question properties

```python
with LimeSurveySession(url, username, password) as client:
    result = client.call('set_question_properties', client.session_key,
                        question_id, {
                            'question': 'Updated question text',
                            'mandatory': 'Y',
                            'help': 'New help text'
                        })
    
    print(result)
```

---

## Export & Reporting

### Generate PDF statistics report

```python
with LimeSurveySession(url, username, password) as client:
    encoded = client.call('export_statistics', client.session_key,
                         survey_id, 'pdf', None, '1')  # With graphs
    
    import base64
    pdf_data = base64.b64decode(encoded)
    
    with open('statistics.pdf', 'wb') as f:
        f.write(pdf_data)
    
    print("Statistics saved to statistics.pdf")
```

### Get submission timeline (daily)

```python
with LimeSurveySession(url, username, password) as client:
    timeline = client.call('export_timeline', client.session_key,
                          survey_id, 'day', '2024-01-01', '2024-12-31')
    
    for entry in timeline:
        print(f"{entry['period']}: {entry['count']} submissions")
```

### Get survey fieldmap for analysis

```python
with LimeSurveySession(url, username, password) as client:
    fieldmap = client.call('get_fieldmap', client.session_key, survey_id)
    
    print("Question codes:")
    for code, info in fieldmap.items():
        print(f"  {code}: {info.get('qid')} - {info.get('title')}")
```

---

## Batch Operations

### Import survey from file

```python
import base64

with open('survey.lss', 'rb') as f:
    encoded = base64.b64encode(f.read()).decode()

with LimeSurveySession(url, username, password) as client:
    new_id = client.call('import_survey', client.session_key,
                        encoded, 'lss', 'Imported Survey')
    
    print(f"Imported survey ID: {new_id}")
```

### Delete old incomplete responses

```python
with LimeSurveySession(url, username, password) as client:
    # Export incomplete to get IDs
    encoded = client.call('export_responses', client.session_key,
                         survey_id, 'json', None, 'incomplete')
    
    import json
    responses = json.loads(client.decode_base64(encoded))
    
    # Delete old ones (example: before 2024)
    deleted = 0
    for resp in responses['responses']:
        submit_date = resp.get('submitdate')
        if submit_date and submit_date < '2024-01-01':
            result = client.call('delete_response', client.session_key,
                               survey_id, resp['id'])
            deleted += 1
    
    print(f"Deleted {deleted} old incomplete responses")
```

---

## Error Handling

### Robust error handling

```python
from limesurvey_client import LimeSurveyError

try:
    with LimeSurveySession(url, username, password) as client:
        result = client.call('activate_survey', client.session_key, survey_id)
        
        # Check for API-level errors
        if isinstance(result, dict) and 'status' in result:
            if 'already active' in result['status'].lower():
                print("Survey is already active")
            else:
                print(f"Error: {result['status']}")
        else:
            print("Success!")
            
except LimeSurveyError as e:
    print(f"LimeSurvey API error: {e}")
except Exception as e:
    print(f"Unexpected error: {e}")
```


path: scripts/limesurvey.py
content: #!/usr/bin/env python3
"""
LimeSurvey CLI tool
Common operations via RemoteControl 2 API
"""

import os
import sys
import argparse
import json
from pathlib import Path

# Add scripts directory to path
script_dir = Path(__file__).parent
sys.path.insert(0, str(script_dir))

from limesurvey_client import LimeSurveyClient, LimeSurveySession, LimeSurveyError


def get_config():
    """Load LimeSurvey configuration from environment or config file"""
    config = {
        'url': os.getenv('LIMESURVEY_URL'),
        'username': os.getenv('LIMESURVEY_USER'),
        'password': os.getenv('LIMESURVEY_PASSWORD'),
    }
    
    # Check for missing config
    missing = [k for k, v in config.items() if not v]
    if missing:
        print(f"Error: Missing configuration: {', '.join(missing)}", file=sys.stderr)
        print("\nSet environment variables:", file=sys.stderr)
        print("  LIMESURVEY_URL - e.g., https://survey.example.com/index.php/admin/remotecontrol", file=sys.stderr)
        print("  LIMESURVEY_USER - your username", file=sys.stderr)
        print("  LIMESURVEY_PASSWORD - your password", file=sys.stderr)
        sys.exit(1)
    
    return config


def cmd_list_surveys(args):
    """List all surveys accessible to the user"""
    config = get_config()
    
    with LimeSurveySession(config['url'], config['username'], config['password']) as client:
        surveys = client.call('list_surveys', client.session_key, args.username)
        
        if not surveys:
            print("No surveys found")
            return
        
        if args.json:
            print(json.dumps(surveys, indent=2))
        else:
            print(f"{'ID':<10} {'Active':<8} {'Title'}")
            print("-" * 80)
            for survey in surveys:
                sid = survey.get('sid', 'N/A')
                active = survey.get('active', 'N/A')
                title = survey.get('surveyls_title', 'N/A')
                print(f"{sid:<10} {active:<8} {title}")


def cmd_export_responses(args):
    """Export survey responses"""
    config = get_config()
    
    with LimeSurveySession(config['url'], config['username'], config['password']) as client:
        result = client.call(
            'export_responses',
            client.session_key,
            args.survey_id,
            args.format,
            args.language or None,
            args.completion_status,
            args.heading_type,
            args.response_type
        )
        
        if isinstance(result, dict) and 'status' in result:
            print(f"Error: {result['status']}", file=sys.stderr)
            sys.exit(1)
        
        # Decode base64 response
        decoded = client.decode_base64(result)
        
        if args.output:
            with open(args.output, 'w', encoding='utf-8') as f:
                f.write(decoded)
            print(f"Exported to: {args.output}")
        else:
            print(decoded)


def cmd_list_participants(args):
    """List survey participants"""
    config = get_config()
    
    with LimeSurveySession(config['url'], config['username'], config['password']) as client:
        participants = client.call(
            'list_participants',
            client.session_key,
            args.survey_id,
            args.start,
            args.limit,
            args.unused,
            args.attributes if args.attributes else False
        )
        
        if not participants:
            print("No participants found")
            return
        
        if args.json:
            print(json.dumps(participants, indent=2))
        else:
            print(f"{'Token ID':<10} {'Token':<20} {'Email'}")
            print("-" * 80)
            for p in participants:
                tid = p.get('tid', 'N/A')
                [REDACTED_SECRET]'token', 'N/A')
                info = p.get('participant_info', {})
                email = info.get('email', 'N/A')
                print(f"{tid:<10} {token:<20} {email}")


def cmd_add_participants(args):
    """Add participants to a survey"""
    config = get_config()
    
    # Read participant data from JSON file or stdin
    if args.file:
        with open(args.file, 'r') as f:
            participant_data = json.load(f)
    else:
        participant_data = json.load(sys.stdin)
    
    if not isinstance(participant_data, list):
        participant_data = [participant_data]
    
    with LimeSurveySession(config['url'], config['username'], config['password']) as client:
        result = client.call(
            'add_participants',
            client.session_key,
            args.survey_id,
            participant_data,
            args.create_token
        )
        
        print(json.dumps(result, indent=2))


def cmd_invite_participants(args):
    """Send invitation emails to participants"""
    config = get_config()
    
    # Parse token IDs
    token_ids = None
    if args.token_ids:
        token_ids = [int(tid) for tid in args.token_ids.split(',')]
    
    with LimeSurveySession(config['url'], config['username'], config['password']) as client:
        result = client.call(
            'invite_participants',
            client.session_key,
            args.survey_id,
            token_ids
        )
        
        print(json.dumps(result, indent=2))


def cmd_activate_survey(args):
    """Activate a survey"""
    config = get_config()
    
    with LimeSurveySession(config['url'], config['username'], config['password']) as client:
        result = client.call(
            'activate_survey',
            client.session_key,
            args.survey_id
        )
        
        if isinstance(result, dict) and 'status' in result:
            print(f"Status: {result['status']}")
        else:
            print(json.dumps(result, indent=2))


def cmd_get_summary(args):
    """Get survey summary statistics"""
    config = get_config()
    
    with LimeSurveySession(config['url'], config['username'], config['password']) as client:
        result = client.call(
            'get_summary',
            client.session_key,
            args.survey_id,
            args.stat_name
        )
        
        if isinstance(result, dict):
            print(json.dumps(result, indent=2))
        else:
            print(f"{args.stat_name}: {result}")


def main():
    parser = argparse.ArgumentParser(
        description='LimeSurvey RemoteControl CLI',
        formatter_class=argparse.RawDescriptionHelpFormatter
    )
    
    subparsers = parser.add_subparsers(dest='command', help='Commands')
    
    # list-surveys
    p_list = subparsers.add_parser('list-surveys', help='List all surveys')
    p_list.add_argument('--username', help='Filter by username (admin only)')
    p_list.add_argument('--json', action='store_true', help='Output as JSON')
    p_list.set_defaults(func=cmd_list_surveys)
    
    # export-responses
    p_export = subparsers.add_parser('export-responses', help='Export survey responses')
    p_export.add_argument('survey_id', type=int, help='Survey ID')
    p_export.add_argument('--format', default='csv', choices=['csv', 'pdf', 'xls', 'json'], help='Export format')
    p_export.add_argument('--language', help='Language code')
    p_export.add_argument('--completion-status', default='all', choices=['all', 'complete', 'incomplete'])
    p_export.add_argument('--heading-type', default='code', choices=['code', 'full', 'abbreviated'])
    p_export.add_argument('--response-type', default='short', choices=['short', 'long'])
    p_export.add_argument('-o', '--output', help='Output file (default: stdout)')
    p_export.set_defaults(func=cmd_export_responses)
    
    # list-participants
    p_participants = subparsers.add_parser('list-participants', help='List survey participants')
    p_participants.add_argument('survey_id', type=int, help='Survey ID')
    p_participants.add_argument('--start', type=int, default=0, help='Start index')
    p_participants.add_argument('--limit', type=int, default=100, help='Max results')
    p_participants.add_argument('--unused', action='store_true', help='Show only unused tokens')
    p_participants.add_argument('--attributes', nargs='+', help='Additional attributes to fetch')
    p_participants.add_argument('--json', action='store_true', help='Output as JSON')
    p_participants.set_defaults(func=cmd_list_participants)
    
    # add-participants
    p_add = subparsers.add_parser('add-participants', help='Add participants to survey')
    p_add.add_argument('survey_id', type=int, help='Survey ID')
    p_add.add_argument('--file', help='JSON file with participant data (default: stdin)')
    p_add.add_argument('--create-token', action='store_true', default=True, help='Create access tokens')
    p_add.set_defaults(func=cmd_add_participants)
    
    # invite-participants
    p_invite = subparsers.add_parser('invite-participants', help='Send invitation emails')
    p_invite.add_argument('survey_id', type=int, help='Survey ID')
    p_invite.add_argument('--token-ids', help='Comma-separated token IDs (default: all)')
    p_invite.set_defaults(func=cmd_invite_participants)
    
    # activate-survey
    p_activate = subparsers.add_parser('activate-survey', help='Activate a survey')
    p_activate.add_argument('survey_id', type=int, help='Survey ID')
    p_activate.set_defaults(func=cmd_activate_survey)
    
    # get-summary
    p_summary = subparsers.add_parser('get-summary', help='Get survey statistics')
    p_summary.add_argument('survey_id', type=int, help='Survey ID')
    p_summary.add_argument('--stat-name', default='all', help='Statistic name (default: all)')
    p_summary.set_defaults(func=cmd_get_summary)
    
    args = parser.parse_args()
    
    if not args.command:
        parser.print_help()
        sys.exit(1)
    
    try:
        args.func(args)
    except LimeSurveyError as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)
    except KeyboardInterrupt:
        print("\nCancelled", file=sys.stderr)
        sys.exit(130)


if __name__ == '__main__':
    main()


path: scripts/limesurvey_client.py
content: #!/usr/bin/env python3
"""
LimeSurvey RemoteControl 2 API Client
JSON-RPC client for LimeSurvey automation
"""

import json
import base64
from urllib import request as urllib_request
from urllib.error import HTTPError, URLError


class LimeSurveyError(Exception):
    """Custom exception for LimeSurvey API errors"""
    pass


class LimeSurveyClient:
    """
    JSON-RPC client for LimeSurvey RemoteControl 2 API
    
    Usage:
        client = LimeSurveyClient('https://survey.example.com/index.php/admin/remotecontrol')
        session_key = client.get_session_key('admin', 'password')
        surveys = client.call('list_surveys', session_key)
        client.release_session_key(session_key)
    """
    
    def __init__(self, url):
        """
        Initialize the LimeSurvey client
        
        Args:
            url: Full URL to the RemoteControl endpoint
                 Example: https://example.com/index.php/admin/remotecontrol
        """
        self.url = url
        self.request_id = 0
        
    def call(self, method, *params):
        """
        Call a RemoteControl API method
        
        Args:
            method: API method name (e.g., 'list_surveys')
            *params: Method parameters
            
        Returns:
            Result from the API call
            
        Raises:
            LimeSurveyError: If the API returns an error
        """
        self.request_id += 1
        
        payload = {
            "jsonrpc": "2.0",
            "method": method,
            "params": list(params),
            "id": self.request_id
        }
        
        data = json.dumps(payload).encode('utf-8')
        
        req = urllib_request.Request(
            self.url,
            data=data,
            headers={
                'Content-Type': 'application/json',
                'Connection': 'Keep-Alive'
            }
        )
        
        try:
            with urllib_request.urlopen(req) as response:
                result = json.loads(response.read().decode('utf-8'))
                
                if 'error' in result and result['error']:
                    raise LimeSurveyError(f"API error: {result['error']}")
                
                return result.get('result')
                
        except HTTPError as e:
            raise LimeSurveyError(f"HTTP error {e.code}: {e.reason}")
        except URLError as e:
            raise LimeSurveyError(f"URL error: {e.reason}")
        except json.JSONDecodeError as e:
            raise LimeSurveyError(f"Invalid JSON response: {e}")
    
    def get_session_key(self, username, password, plugin='Authdb'):
        """
        Create a new session key
        
        Args:
            username: LimeSurvey username
            [REDACTED_SECRET] password
            plugin: Authentication plugin (default: 'Authdb')
            
        Returns:
            Session key string
            
        Raises:
            LimeSurveyError: If authentication fails
        """
        result = self.call('get_session_key', username, password, plugin)
        
        # Check for authentication failure
        if isinstance(result, dict) and 'status' in result:
            raise LimeSurveyError(f"Authentication failed: {result['status']}")
        
        return result
    
    def release_session_key(self, session_key):
        """
        Close the RPC session
        
        Args:
            session_key: Active session key
            
        Returns:
            'OK' on success
        """
        return self.call('release_session_key', session_key)
    
    def decode_base64(self, encoded_data):
        """
        Decode base64-encoded data from export functions
        
        Args:
            encoded_data: Base64-encoded string
            
        Returns:
            Decoded string
        """
        return base64.b64decode(encoded_data).decode('utf-8')


# Context manager support
class LimeSurveySession:
    """
    Context manager for automatic session handling
    
    Usage:
        with LimeSurveySession(url, username, password) as client:
            surveys = client.call('list_surveys', client.session_key)
    """
    
    def __init__(self, url, username, password, plugin='Authdb'):
        self.client = LimeSurveyClient(url)
        self.username = username
        self.[REDACTED_SECRET]
        self.plugin = plugin
        self.session_key = None
        
    def __enter__(self):
        self.session_key = self.client.get_session_key(
            self.username, 
            self.password, 
            self.plugin
        )
        self.client.session_key = self.session_key  # Store for convenience
        return self.client
        
    def __exit__(self, exc_type, exc_val, exc_tb):
        if self.session_key:
            try:
                self.client.release_session_key(self.session_key)
            except:
                pass  # Ignore errors on cleanup
        return False


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