or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

autocomplete-paths.mdcore-classes.mdexecution-control.mdforms.mdindex.mdselection-prompts.mdtext-input.md

forms.mddocs/

0

# Forms and Multi-Question Workflows

1

2

Forms enable multi-question workflows that execute a series of prompts and return consolidated results, supporting field validation, conditional logic, and both synchronous and asynchronous execution.

3

4

## Capabilities

5

6

### Form Creation

7

8

Create forms from multiple Question instances with named fields for organized data collection.

9

10

```python { .api }

11

def form(**kwargs: Question) -> Form:

12

"""

13

Create a form from keyword arguments mapping field names to Questions.

14

15

Args:

16

**kwargs: Field names mapped to Question instances

17

18

Returns:

19

Form instance ready for execution

20

"""

21

22

class FormField(NamedTuple):

23

key: str

24

question: Question

25

"""

26

Named tuple representing a question within a form.

27

28

Attributes:

29

key: Field identifier for result dictionary

30

question: Question instance to execute

31

"""

32

```

33

34

### Form Execution

35

36

Execute forms with comprehensive error handling and result consolidation.

37

38

```python { .api }

39

class Form:

40

def __init__(self, *form_fields: FormField) -> None:

41

"""

42

Initialize form with FormField instances.

43

44

Args:

45

*form_fields: FormField instances defining the form structure

46

"""

47

48

def ask(self, patch_stdout: bool = False,

49

kbi_msg: str = DEFAULT_KBI_MESSAGE) -> Dict[str, Any]:

50

"""

51

Execute form synchronously with error handling.

52

53

Args:

54

patch_stdout: Patch stdout to prevent interference

55

kbi_msg: Message displayed on keyboard interrupt

56

57

Returns:

58

Dictionary mapping field keys to user responses

59

60

Raises:

61

KeyboardInterrupt: User cancelled with appropriate message

62

"""

63

64

def unsafe_ask(self, patch_stdout: bool = False) -> Dict[str, Any]:

65

"""

66

Execute form synchronously without error handling.

67

68

Args:

69

patch_stdout: Patch stdout to prevent interference

70

71

Returns:

72

Dictionary mapping field keys to user responses

73

"""

74

75

def ask_async(self, patch_stdout: bool = False,

76

kbi_msg: str = DEFAULT_KBI_MESSAGE) -> Dict[str, Any]:

77

"""

78

Execute form asynchronously with error handling.

79

80

Args:

81

patch_stdout: Patch stdout to prevent interference

82

kbi_msg: Message displayed on keyboard interrupt

83

84

Returns:

85

Dictionary mapping field keys to user responses

86

"""

87

88

def unsafe_ask_async(self, patch_stdout: bool = False) -> Dict[str, Any]:

89

"""

90

Execute form asynchronously without error handling.

91

92

Args:

93

patch_stdout: Patch stdout to prevent interference

94

95

Returns:

96

Dictionary mapping field keys to user responses

97

"""

98

```

99

100

## Usage Examples

101

102

### Basic Form Creation

103

104

```python

105

import questionary

106

107

# Create form using keyword arguments

108

user_info = questionary.form(

109

name=questionary.text("Enter your name:"),

110

email=questionary.text("Enter your email:"),

111

age=questionary.text("Enter your age:"),

112

confirmed=questionary.confirm("Is this information correct?")

113

).ask()

114

115

print(f"Name: {user_info['name']}")

116

print(f"Email: {user_info['email']}")

117

print(f"Age: {user_info['age']}")

118

print(f"Confirmed: {user_info['confirmed']}")

119

```

120

121

### Advanced Form with Validation

122

123

```python

124

import questionary

125

from questionary import Validator, ValidationError

126

127

# Custom validators

128

class EmailValidator(Validator):

129

def validate(self, document):

130

if "@" not in document.text:

131

raise ValidationError(message="Please enter a valid email")

132

133

class AgeValidator(Validator):

134

def validate(self, document):

135

try:

136

age = int(document.text)

137

if age < 0 or age > 120:

138

raise ValidationError(message="Please enter a valid age (0-120)")

139

except ValueError:

140

raise ValidationError(message="Please enter a number")

141

142

# Form with validated fields

143

registration = questionary.form(

144

username=questionary.text(

145

"Choose username:",

146

validate=lambda x: "Username too short" if len(x) < 3 else True

147

),

148

email=questionary.text(

149

"Enter email:",

150

validate=EmailValidator()

151

),

152

age=questionary.text(

153

"Enter age:",

154

validate=AgeValidator()

155

),

156

terms=questionary.confirm(

157

"Do you accept the terms?",

158

default=False

159

)

160

).ask()

161

162

if not registration['terms']:

163

print("Registration cancelled - terms not accepted")

164

else:

165

print("Registration successful!")

166

```

167

168

### Complex Form with Conditional Logic

169

170

```python

171

import questionary

172

173

# First collect basic info

174

basic_info = questionary.form(

175

user_type=questionary.select(

176

"Account type:",

177

choices=["personal", "business", "developer"]

178

),

179

name=questionary.text("Full name:")

180

).ask()

181

182

# Conditional follow-up questions based on user type

183

if basic_info['user_type'] == 'business':

184

business_info = questionary.form(

185

company=questionary.text("Company name:"),

186

employees=questionary.select(

187

"Company size:",

188

choices=["1-10", "11-50", "51-200", "200+"]

189

),

190

industry=questionary.text("Industry:")

191

).ask()

192

193

# Merge results

194

result = {**basic_info, **business_info}

195

196

elif basic_info['user_type'] == 'developer':

197

dev_info = questionary.form(

198

languages=questionary.checkbox(

199

"Programming languages:",

200

choices=["Python", "JavaScript", "Java", "C++", "Go", "Rust"]

201

),

202

experience=questionary.select(

203

"Years of experience:",

204

choices=["0-2", "3-5", "6-10", "10+"]

205

)

206

).ask()

207

208

result = {**basic_info, **dev_info}

209

210

else:

211

result = basic_info

212

213

print("Collected information:", result)

214

```

215

216

### Async Form Execution

217

218

```python

219

import questionary

220

import asyncio

221

222

async def async_form_example():

223

# Forms can be executed asynchronously

224

config = await questionary.form(

225

host=questionary.text("Database host:", default="localhost"),

226

port=questionary.text("Database port:", default="5432"),

227

database=questionary.text("Database name:"),

228

ssl=questionary.confirm("Use SSL?", default=True)

229

).ask_async()

230

231

print(f"Connecting to {config['host']}:{config['port']}")

232

return config

233

234

# Run async form

235

# config = asyncio.run(async_form_example())

236

```

237

238

### Form with Manual FormField Construction

239

240

```python

241

import questionary

242

from questionary import FormField

243

244

# Manual FormField creation for more control

245

form_fields = [

246

FormField("project_name", questionary.text("Project name:")),

247

FormField("description", questionary.text("Description:", multiline=True)),

248

FormField("license", questionary.select(

249

"License:",

250

choices=["MIT", "Apache-2.0", "GPL-3.0", "BSD-3-Clause"]

251

)),

252

FormField("public", questionary.confirm("Make repository public?"))

253

]

254

255

project_form = questionary.Form(*form_fields)

256

project_config = project_form.ask()

257

258

print("Project configuration:")

259

for key, value in project_config.items():

260

print(f" {key}: {value}")

261

```

262

263

### Error Handling in Forms

264

265

```python

266

import questionary

267

268

try:

269

settings = questionary.form(

270

debug=questionary.confirm("Enable debug mode?"),

271

log_level=questionary.select(

272

"Log level:",

273

choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]

274

),

275

max_connections=questionary.text(

276

"Max connections:",

277

validate=lambda x: "Must be a number" if not x.isdigit() else True

278

)

279

).ask()

280

281

print("Settings configured:", settings)

282

283

except KeyboardInterrupt:

284

print("\nConfiguration cancelled by user")

285

except Exception as e:

286

print(f"Configuration error: {e}")

287

```

288

289

### Form with Skip Logic

290

291

```python

292

import questionary

293

294

# Initial question to determine workflow

295

workflow = questionary.select(

296

"What would you like to do?",

297

choices=["create", "update", "delete"]

298

).ask()

299

300

if workflow == "create":

301

create_form = questionary.form(

302

name=questionary.text("Resource name:"),

303

type=questionary.select(

304

"Resource type:",

305

choices=["database", "server", "storage"]

306

),

307

region=questionary.select(

308

"Region:",

309

choices=["us-east-1", "us-west-2", "eu-west-1"]

310

)

311

).ask()

312

313

print(f"Creating {create_form['type']} '{create_form['name']}' in {create_form['region']}")

314

315

elif workflow == "update":

316

update_form = questionary.form(

317

resource_id=questionary.text("Resource ID to update:"),

318

changes=questionary.checkbox(

319

"What to update:",

320

choices=["configuration", "scaling", "security", "tags"]

321

)

322

).ask()

323

324

print(f"Updating resource {update_form['resource_id']}: {update_form['changes']}")

325

326

elif workflow == "delete":

327

delete_form = questionary.form(

328

resource_id=questionary.text("Resource ID to delete:"),

329

confirmation=questionary.text(

330

"Type 'DELETE' to confirm:",

331

validate=lambda x: True if x == "DELETE" else "Please type 'DELETE' to confirm"

332

)

333

).ask()

334

335

if delete_form['confirmation'] == "DELETE":

336

print(f"Deleting resource {delete_form['resource_id']}")

337

```